mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-18 21:59:54 +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.assertEquals;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class BlogManagerTest {
|
public class BlogManagerTest extends BriarIntegrationTest {
|
||||||
|
|
||||||
private LifecycleManager lifecycleManager0, lifecycleManager1;
|
private LifecycleManager lifecycleManager0, lifecycleManager1;
|
||||||
private SyncSessionFactory sync0, sync1;
|
private SyncSessionFactory sync0, sync1;
|
||||||
@@ -94,7 +94,7 @@ public class BlogManagerTest {
|
|||||||
private final String AUTHOR2 = "Author 2";
|
private final String AUTHOR2 = "Author 2";
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
Logger.getLogger(ForumSharingIntegrationTest.class.getName());
|
Logger.getLogger(BlogManagerTest.class.getName());
|
||||||
|
|
||||||
private BlogManagerTestComponent t0, t1;
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/layout"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/forum_cell"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal"
|
||||||
|
android:baselineAligned="false">
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:layout_width="wrap_content"
|
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_invite_members">Invite Members</string>
|
||||||
<string name="groups_leave">Leave Group</string>
|
<string name="groups_leave">Leave Group</string>
|
||||||
<string name="groups_dissolve">Dissolve Group</string>
|
<string name="groups_dissolve">Dissolve Group</string>
|
||||||
|
<string name="groups_member_joined">joined the group.</string>
|
||||||
|
|
||||||
<!-- Private Group Invitations -->
|
<!-- Private Group Invitations -->
|
||||||
<string name="groups_invitations_title">Group Invitations</string>
|
<string name="groups_invitations_title">Group Invitations</string>
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ public class ActivityModule {
|
|||||||
@Provides
|
@Provides
|
||||||
protected GroupController provideGroupController(
|
protected GroupController provideGroupController(
|
||||||
GroupControllerImpl groupController) {
|
GroupControllerImpl groupController) {
|
||||||
|
activity.addLifecycleController(groupController);
|
||||||
return groupController;
|
return groupController;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ import org.briarproject.api.messaging.MessagingManager;
|
|||||||
import org.briarproject.api.messaging.PrivateMessageFactory;
|
import org.briarproject.api.messaging.PrivateMessageFactory;
|
||||||
import org.briarproject.api.plugins.ConnectionRegistry;
|
import org.briarproject.api.plugins.ConnectionRegistry;
|
||||||
import org.briarproject.api.plugins.PluginManager;
|
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.PrivateGroupManager;
|
||||||
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
||||||
import org.briarproject.api.settings.SettingsManager;
|
import org.briarproject.api.settings.SettingsManager;
|
||||||
@@ -99,6 +101,10 @@ public interface AndroidComponent extends CoreEagerSingletons {
|
|||||||
|
|
||||||
GroupInvitationManager groupInvitationManager();
|
GroupInvitationManager groupInvitationManager();
|
||||||
|
|
||||||
|
PrivateGroupFactory privateGroupFactory();
|
||||||
|
|
||||||
|
GroupMessageFactory groupMessageFactory();
|
||||||
|
|
||||||
ForumManager forumManager();
|
ForumManager forumManager();
|
||||||
|
|
||||||
ForumSharingManager forumSharingManager();
|
ForumSharingManager forumSharingManager();
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ import android.widget.Toast;
|
|||||||
import org.briarproject.R;
|
import org.briarproject.R;
|
||||||
import org.briarproject.android.ActivityComponent;
|
import org.briarproject.android.ActivityComponent;
|
||||||
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
||||||
import org.briarproject.android.sharing.ShareForumActivity;
|
|
||||||
import org.briarproject.android.sharing.ForumSharingStatusActivity;
|
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.ThreadListActivity;
|
||||||
import org.briarproject.android.threaded.ThreadListController;
|
import org.briarproject.android.threaded.ThreadListController;
|
||||||
import org.briarproject.api.db.DbException;
|
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;
|
import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
|
||||||
|
|
||||||
public class ForumActivity extends
|
public class ForumActivity extends
|
||||||
ThreadListActivity<Forum, ForumItem, ForumPostHeader, NestedForumAdapter> {
|
ThreadListActivity<Forum, ForumItem, ForumPostHeader> {
|
||||||
|
|
||||||
private static final int REQUEST_FORUM_SHARED = 3;
|
private static final int REQUEST_FORUM_SHARED = 3;
|
||||||
|
|
||||||
@@ -74,9 +75,9 @@ public class ForumActivity extends
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected NestedForumAdapter createAdapter(
|
protected ThreadItemAdapter<ForumItem> createAdapter(
|
||||||
LinearLayoutManager layoutManager) {
|
LinearLayoutManager layoutManager) {
|
||||||
return new NestedForumAdapter(this, layoutManager);
|
return new ThreadItemAdapter<>(this, layoutManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.briarproject.android.forum;
|
|||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import org.briarproject.android.api.AndroidNotificationManager;
|
import org.briarproject.android.api.AndroidNotificationManager;
|
||||||
|
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
||||||
import org.briarproject.android.threaded.ThreadListControllerImpl;
|
import org.briarproject.android.threaded.ThreadListControllerImpl;
|
||||||
import org.briarproject.api.clients.MessageTracker.GroupCount;
|
import org.briarproject.api.clients.MessageTracker.GroupCount;
|
||||||
import org.briarproject.api.crypto.CryptoExecutor;
|
import org.briarproject.api.crypto.CryptoExecutor;
|
||||||
@@ -28,8 +29,11 @@ import java.util.logging.Logger;
|
|||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
public class ForumControllerImpl extends
|
import static java.lang.Math.max;
|
||||||
ThreadListControllerImpl<Forum, ForumItem, ForumPostHeader, ForumPost>
|
import static java.util.logging.Level.WARNING;
|
||||||
|
|
||||||
|
public class ForumControllerImpl
|
||||||
|
extends ThreadListControllerImpl<Forum, ForumItem, ForumPostHeader, ForumPost>
|
||||||
implements ForumController {
|
implements ForumController {
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
@@ -42,9 +46,9 @@ public class ForumControllerImpl extends
|
|||||||
LifecycleManager lifecycleManager, IdentityManager identityManager,
|
LifecycleManager lifecycleManager, IdentityManager identityManager,
|
||||||
@CryptoExecutor Executor cryptoExecutor,
|
@CryptoExecutor Executor cryptoExecutor,
|
||||||
ForumManager forumManager, EventBus eventBus,
|
ForumManager forumManager, EventBus eventBus,
|
||||||
AndroidNotificationManager notificationManager, Clock clock) {
|
Clock clock, AndroidNotificationManager notificationManager) {
|
||||||
super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor,
|
super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor,
|
||||||
eventBus, notificationManager, clock);
|
eventBus, clock, notificationManager);
|
||||||
this.forumManager = forumManager;
|
this.forumManager = forumManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,8 +88,8 @@ public class ForumControllerImpl extends
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String loadMessageBody(MessageId id) throws DbException {
|
protected String loadMessageBody(ForumPostHeader h) throws DbException {
|
||||||
return StringUtils.fromUtf8(forumManager.getPostBody(id));
|
return StringUtils.fromUtf8(forumManager.getPostBody(h.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -94,16 +98,42 @@ public class ForumControllerImpl extends
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected long getLatestTimestamp() throws DbException {
|
public void createAndStoreMessage(final String body,
|
||||||
GroupCount count = forumManager.getGroupCount(getGroupId());
|
@Nullable final ForumItem parentItem,
|
||||||
return count.getLatestMsgTime();
|
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
|
private void createMessage(final String body, final long timestamp,
|
||||||
protected ForumPost createLocalMessage(String body, long timestamp,
|
final @Nullable MessageId parentId, final LocalAuthor author,
|
||||||
@Nullable MessageId parentId, LocalAuthor author) {
|
final ResultExceptionHandler<ForumItem, DbException> handler) {
|
||||||
return forumManager.createLocalPost(getGroupId(), body, timestamp,
|
cryptoExecutor.execute(new Runnable() {
|
||||||
parentId, author);
|
@Override
|
||||||
|
public void run() {
|
||||||
|
LOG.info("Creating forum post...");
|
||||||
|
ForumPost msg = forumManager
|
||||||
|
.createLocalPost(getGroupId(), body, timestamp,
|
||||||
|
parentId, author);
|
||||||
|
storePost(msg, body, handler);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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;
|
import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_BODY_LENGTH;
|
||||||
|
|
||||||
public class GroupActivity extends
|
public class GroupActivity extends
|
||||||
ThreadListActivity<PrivateGroup, GroupMessageItem, GroupMessageHeader, GroupMessageAdapter> {
|
ThreadListActivity<PrivateGroup, GroupMessageItem, GroupMessageHeader> {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
GroupController controller;
|
GroupController controller;
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package org.briarproject.android.privategroup.conversation;
|
|||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import org.briarproject.android.api.AndroidNotificationManager;
|
import org.briarproject.android.api.AndroidNotificationManager;
|
||||||
|
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
||||||
import org.briarproject.android.threaded.ThreadListControllerImpl;
|
import org.briarproject.android.threaded.ThreadListControllerImpl;
|
||||||
import org.briarproject.api.clients.MessageTracker.GroupCount;
|
|
||||||
import org.briarproject.api.crypto.CryptoExecutor;
|
import org.briarproject.api.crypto.CryptoExecutor;
|
||||||
import org.briarproject.api.db.DatabaseExecutor;
|
import org.briarproject.api.db.DatabaseExecutor;
|
||||||
import org.briarproject.api.db.DbException;
|
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.identity.LocalAuthor;
|
||||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||||
import org.briarproject.api.privategroup.GroupMessage;
|
import org.briarproject.api.privategroup.GroupMessage;
|
||||||
|
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||||
import org.briarproject.api.privategroup.GroupMessageHeader;
|
import org.briarproject.api.privategroup.GroupMessageHeader;
|
||||||
|
import org.briarproject.api.privategroup.JoinMessageHeader;
|
||||||
import org.briarproject.api.privategroup.PrivateGroup;
|
import org.briarproject.api.privategroup.PrivateGroup;
|
||||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
@@ -27,6 +29,9 @@ import java.util.logging.Logger;
|
|||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import static java.lang.Math.max;
|
||||||
|
import static java.util.logging.Level.WARNING;
|
||||||
|
|
||||||
public class GroupControllerImpl extends
|
public class GroupControllerImpl extends
|
||||||
ThreadListControllerImpl<PrivateGroup, GroupMessageItem, GroupMessageHeader, GroupMessage>
|
ThreadListControllerImpl<PrivateGroup, GroupMessageItem, GroupMessageHeader, GroupMessage>
|
||||||
implements GroupController {
|
implements GroupController {
|
||||||
@@ -35,16 +40,19 @@ public class GroupControllerImpl extends
|
|||||||
Logger.getLogger(GroupControllerImpl.class.getName());
|
Logger.getLogger(GroupControllerImpl.class.getName());
|
||||||
|
|
||||||
private final PrivateGroupManager privateGroupManager;
|
private final PrivateGroupManager privateGroupManager;
|
||||||
|
private final GroupMessageFactory groupMessageFactory;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
GroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
GroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||||
LifecycleManager lifecycleManager, IdentityManager identityManager,
|
LifecycleManager lifecycleManager, IdentityManager identityManager,
|
||||||
@CryptoExecutor Executor cryptoExecutor,
|
@CryptoExecutor Executor cryptoExecutor,
|
||||||
PrivateGroupManager privateGroupManager, EventBus eventBus,
|
PrivateGroupManager privateGroupManager,
|
||||||
AndroidNotificationManager notificationManager, Clock clock) {
|
GroupMessageFactory groupMessageFactory, EventBus eventBus,
|
||||||
|
Clock clock, AndroidNotificationManager notificationManager) {
|
||||||
super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor,
|
super(dbExecutor, lifecycleManager, identityManager, cryptoExecutor,
|
||||||
eventBus, notificationManager, clock);
|
eventBus, clock, notificationManager);
|
||||||
this.privateGroupManager = privateGroupManager;
|
this.privateGroupManager = privateGroupManager;
|
||||||
|
this.groupMessageFactory = groupMessageFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -83,8 +91,13 @@ public class GroupControllerImpl extends
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String loadMessageBody(MessageId id) throws DbException {
|
protected String loadMessageBody(GroupMessageHeader header)
|
||||||
return privateGroupManager.getMessageBody(id);
|
throws DbException {
|
||||||
|
if (header instanceof JoinMessageHeader) {
|
||||||
|
// will be looked up later
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return privateGroupManager.getMessageBody(header.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -93,16 +106,52 @@ public class GroupControllerImpl extends
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected long getLatestTimestamp() throws DbException {
|
public void createAndStoreMessage(final String body,
|
||||||
GroupCount count = privateGroupManager.getGroupCount(getGroupId());
|
@Nullable final GroupMessageItem parentItem,
|
||||||
return count.getLatestMsgTime();
|
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
|
private void createMessage(final String body, final long timestamp,
|
||||||
protected GroupMessage createLocalMessage(String body, long timestamp,
|
final @Nullable MessageId parentId, final LocalAuthor author,
|
||||||
@Nullable MessageId parentId, LocalAuthor author) {
|
final MessageId previousMsgId,
|
||||||
return privateGroupManager.createLocalMessage(getGroupId(), body,
|
final ResultExceptionHandler<GroupMessageItem, DbException> handler) {
|
||||||
timestamp, parentId, author);
|
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
|
@Override
|
||||||
@@ -119,6 +168,9 @@ public class GroupControllerImpl extends
|
|||||||
@Override
|
@Override
|
||||||
protected GroupMessageItem buildItem(GroupMessageHeader header,
|
protected GroupMessageItem buildItem(GroupMessageHeader header,
|
||||||
String body) {
|
String body) {
|
||||||
|
if (header instanceof JoinMessageHeader) {
|
||||||
|
return new JoinMessageItem(header, body);
|
||||||
|
}
|
||||||
return new GroupMessageItem(header, body);
|
return new GroupMessageItem(header, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.briarproject.android.privategroup.conversation;
|
package org.briarproject.android.privategroup.conversation;
|
||||||
|
|
||||||
|
import android.support.annotation.LayoutRes;
|
||||||
import android.support.annotation.UiThread;
|
import android.support.annotation.UiThread;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@@ -7,7 +8,9 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import org.briarproject.R;
|
import org.briarproject.R;
|
||||||
|
import org.briarproject.android.threaded.BaseThreadItemViewHolder;
|
||||||
import org.briarproject.android.threaded.ThreadItemAdapter;
|
import org.briarproject.android.threaded.ThreadItemAdapter;
|
||||||
|
import org.briarproject.android.threaded.ThreadPostViewHolder;
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
public class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
|
public class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
|
||||||
@@ -17,12 +20,23 @@ public class GroupMessageAdapter extends ThreadItemAdapter<GroupMessageItem> {
|
|||||||
super(listener, layoutManager);
|
super(listener, layoutManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@LayoutRes
|
||||||
@Override
|
@Override
|
||||||
public GroupMessageViewHolder onCreateViewHolder(ViewGroup parent,
|
public int getItemViewType(int position) {
|
||||||
int viewType) {
|
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())
|
View v = LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.list_item_forum_post, parent, false);
|
.inflate(type, parent, false);
|
||||||
return new GroupMessageViewHolder(v);
|
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;
|
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.android.threaded.ThreadItem;
|
||||||
import org.briarproject.api.identity.Author;
|
import org.briarproject.api.identity.Author;
|
||||||
import org.briarproject.api.identity.Author.Status;
|
import org.briarproject.api.identity.Author.Status;
|
||||||
import org.briarproject.api.privategroup.GroupMessageHeader;
|
import org.briarproject.api.privategroup.GroupMessageHeader;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.NotThreadSafe;
|
||||||
|
|
||||||
|
@UiThread
|
||||||
|
@NotThreadSafe
|
||||||
class GroupMessageItem extends ThreadItem {
|
class GroupMessageItem extends ThreadItem {
|
||||||
|
|
||||||
GroupMessageItem(MessageId messageId, MessageId parentId,
|
private GroupMessageItem(MessageId messageId, MessageId parentId,
|
||||||
String text, long timestamp, Author author, Status status,
|
String text, long timestamp, Author author, Status status,
|
||||||
boolean isRead) {
|
boolean isRead) {
|
||||||
super(messageId, parentId, text, timestamp, author, status, isRead);
|
super(messageId, parentId, text, timestamp, author, status, isRead);
|
||||||
@@ -19,4 +27,9 @@ class GroupMessageItem extends ThreadItem {
|
|||||||
h.getAuthorStatus(), h.isRead());
|
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.DbControllerImpl;
|
||||||
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
||||||
import org.briarproject.api.contact.ContactId;
|
import org.briarproject.api.contact.ContactId;
|
||||||
|
import org.briarproject.api.crypto.CryptoExecutor;
|
||||||
import org.briarproject.api.db.DatabaseExecutor;
|
import org.briarproject.api.db.DatabaseExecutor;
|
||||||
import org.briarproject.api.db.DbException;
|
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.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.privategroup.PrivateGroupManager;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
import org.briarproject.api.system.Clock;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
@@ -23,25 +31,81 @@ public class CreateGroupControllerImpl extends DbControllerImpl
|
|||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
Logger.getLogger(CreateGroupControllerImpl.class.getName());
|
Logger.getLogger(CreateGroupControllerImpl.class.getName());
|
||||||
|
|
||||||
|
private final IdentityManager identityManager;
|
||||||
|
private final PrivateGroupFactory groupFactory;
|
||||||
|
private final GroupMessageFactory groupMessageFactory;
|
||||||
private final PrivateGroupManager groupManager;
|
private final PrivateGroupManager groupManager;
|
||||||
|
private final Clock clock;
|
||||||
|
@CryptoExecutor
|
||||||
|
private final Executor cryptoExecutor;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
CreateGroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
CreateGroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||||
LifecycleManager lifecycleManager,
|
@CryptoExecutor Executor cryptoExecutor,
|
||||||
PrivateGroupManager groupManager) {
|
LifecycleManager lifecycleManager, IdentityManager identityManager,
|
||||||
|
PrivateGroupFactory groupFactory,
|
||||||
|
GroupMessageFactory groupMessageFactory,
|
||||||
|
PrivateGroupManager groupManager, Clock clock) {
|
||||||
super(dbExecutor, lifecycleManager);
|
super(dbExecutor, lifecycleManager);
|
||||||
|
this.identityManager = identityManager;
|
||||||
|
this.groupFactory = groupFactory;
|
||||||
|
this.groupMessageFactory = groupMessageFactory;
|
||||||
this.groupManager = groupManager;
|
this.groupManager = groupManager;
|
||||||
|
this.clock = clock;
|
||||||
|
this.cryptoExecutor = cryptoExecutor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void createGroup(final String name,
|
public void createGroup(final String name,
|
||||||
final ResultExceptionHandler<GroupId, DbException> handler) {
|
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() {
|
runOnDbThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
LOG.info("Adding group to database...");
|
LOG.info("Adding group to database...");
|
||||||
try {
|
try {
|
||||||
handler.onResult(groupManager.addPrivateGroup(name));
|
groupManager.addPrivateGroup(group, newMemberMsg, joinMsg);
|
||||||
|
handler.onResult(group.getId());
|
||||||
} catch (DbException e) {
|
} catch (DbException e) {
|
||||||
if (LOG.isLoggable(WARNING))
|
if (LOG.isLoggable(WARNING))
|
||||||
LOG.log(WARNING, e.toString(), e);
|
LOG.log(WARNING, e.toString(), e);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.animation.ArgbEvaluator;
|
|||||||
import android.animation.ValueAnimator;
|
import android.animation.ValueAnimator;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.drawable.ColorDrawable;
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.support.annotation.CallSuper;
|
||||||
import android.support.annotation.UiThread;
|
import android.support.annotation.UiThread;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
@@ -15,47 +16,32 @@ import android.widget.TextView;
|
|||||||
import org.briarproject.R;
|
import org.briarproject.R;
|
||||||
import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
|
import org.briarproject.android.threaded.ThreadItemAdapter.ThreadItemListener;
|
||||||
import org.briarproject.android.view.AuthorView;
|
import org.briarproject.android.view.AuthorView;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.util.StringUtils;
|
import org.briarproject.util.StringUtils;
|
||||||
|
|
||||||
import static android.view.View.GONE;
|
|
||||||
import static android.view.View.INVISIBLE;
|
|
||||||
import static android.view.View.VISIBLE;
|
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
public abstract class ThreadItemViewHolder<I extends ThreadItem>
|
@NotNullByDefault
|
||||||
|
public abstract class BaseThreadItemViewHolder<I extends ThreadItem>
|
||||||
extends RecyclerView.ViewHolder {
|
extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
private final static int ANIMATION_DURATION = 5000;
|
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 AuthorView author;
|
||||||
private final View[] lvls;
|
|
||||||
private final View chevron, replyButton;
|
|
||||||
private final ViewGroup cell;
|
|
||||||
private final View topDivider;
|
private final View topDivider;
|
||||||
|
|
||||||
public ThreadItemViewHolder(View v) {
|
public BaseThreadItemViewHolder(View v) {
|
||||||
super(v);
|
super(v);
|
||||||
|
|
||||||
|
layout = (ViewGroup) v.findViewById(R.id.layout);
|
||||||
textView = (TextView) v.findViewById(R.id.text);
|
textView = (TextView) v.findViewById(R.id.text);
|
||||||
lvlText = (TextView) v.findViewById(R.id.nested_line_text);
|
|
||||||
author = (AuthorView) v.findViewById(R.id.author);
|
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);
|
topDivider = v.findViewById(R.id.top_divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO improve encapsulation, so we don't need to pass the adapter here
|
// TODO improve encapsulation, so we don't need to pass the adapter here
|
||||||
|
@CallSuper
|
||||||
public void bind(final ThreadItemAdapter<I> adapter,
|
public void bind(final ThreadItemAdapter<I> adapter,
|
||||||
final ThreadItemListener<I> listener, final I item, int pos) {
|
final ThreadItemListener<I> listener, final I item, int pos) {
|
||||||
|
|
||||||
@@ -67,68 +53,22 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
|
|||||||
topDivider.setVisibility(View.VISIBLE);
|
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.setAuthor(item.getAuthor());
|
||||||
author.setDate(item.getTimestamp());
|
author.setDate(item.getTimestamp());
|
||||||
author.setAuthorStatus(item.getStatus());
|
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())) {
|
if (item.equals(adapter.getReplyItem())) {
|
||||||
cell.setBackgroundColor(ContextCompat
|
layout.setBackgroundColor(ContextCompat
|
||||||
.getColor(getContext(), R.color.forum_cell_highlight));
|
.getColor(getContext(), R.color.forum_cell_highlight));
|
||||||
} else if (item.equals(adapter.getAddedItem())) {
|
} else if (item.equals(adapter.getAddedItem())) {
|
||||||
cell.setBackgroundColor(ContextCompat
|
layout.setBackgroundColor(ContextCompat
|
||||||
.getColor(getContext(), R.color.forum_cell_highlight));
|
.getColor(getContext(), R.color.forum_cell_highlight));
|
||||||
animateFadeOut(adapter, adapter.getAddedItem());
|
animateFadeOut(adapter, adapter.getAddedItem());
|
||||||
adapter.clearAddedItem();
|
adapter.clearAddedItem();
|
||||||
} else {
|
} else {
|
||||||
cell.setBackgroundColor(ContextCompat
|
layout.setBackgroundColor(ContextCompat
|
||||||
.getColor(getContext(), R.color.window_background));
|
.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,
|
private void animateFadeOut(final ThreadItemAdapter<I> adapter,
|
||||||
@@ -137,7 +77,7 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
|
|||||||
setIsRecyclable(false);
|
setIsRecyclable(false);
|
||||||
ValueAnimator anim = new ValueAnimator();
|
ValueAnimator anim = new ValueAnimator();
|
||||||
adapter.addAnimatingItem(addedItem, anim);
|
adapter.addAnimatingItem(addedItem, anim);
|
||||||
ColorDrawable viewColor = (ColorDrawable) cell.getBackground();
|
ColorDrawable viewColor = (ColorDrawable) layout.getBackground();
|
||||||
anim.setIntValues(viewColor.getColor(), ContextCompat
|
anim.setIntValues(viewColor.getColor(), ContextCompat
|
||||||
.getColor(getContext(), R.color.window_background));
|
.getColor(getContext(), R.color.window_background));
|
||||||
anim.setEvaluator(new ArgbEvaluator());
|
anim.setEvaluator(new ArgbEvaluator());
|
||||||
@@ -167,7 +107,7 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
|
|||||||
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onAnimationUpdate(ValueAnimator valueAnimator) {
|
public void onAnimationUpdate(ValueAnimator valueAnimator) {
|
||||||
cell.setBackgroundColor(
|
layout.setBackgroundColor(
|
||||||
(Integer) valueAnimator.getAnimatedValue());
|
(Integer) valueAnimator.getAnimatedValue());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -175,7 +115,7 @@ public abstract class ThreadItemViewHolder<I extends ThreadItem>
|
|||||||
anim.start();
|
anim.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Context getContext() {
|
protected Context getContext() {
|
||||||
return textView.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.identity.Author.Status;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.NotThreadSafe;
|
||||||
|
|
||||||
import static org.briarproject.android.threaded.ThreadItemAdapter.UNDEFINED;
|
import static org.briarproject.android.threaded.ThreadItemAdapter.UNDEFINED;
|
||||||
|
|
||||||
/* This class is not thread safe */
|
@NotThreadSafe
|
||||||
public abstract class ThreadItem implements MessageNode {
|
public abstract class ThreadItem implements MessageNode {
|
||||||
|
|
||||||
private final MessageId messageId;
|
private final MessageId messageId;
|
||||||
@@ -92,4 +94,5 @@ public abstract class ThreadItem implements MessageNode {
|
|||||||
public void setDescendantCount(int descendantCount) {
|
public void setDescendantCount(int descendantCount) {
|
||||||
this.descendantCount = descendantCount;
|
this.descendantCount = descendantCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import android.support.annotation.Nullable;
|
|||||||
import android.support.annotation.UiThread;
|
import android.support.annotation.UiThread;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
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.android.util.VersionedAdapter;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
@@ -17,8 +21,8 @@ import java.util.Map;
|
|||||||
|
|
||||||
import static android.support.v7.widget.RecyclerView.NO_POSITION;
|
import static android.support.v7.widget.RecyclerView.NO_POSITION;
|
||||||
|
|
||||||
public abstract class ThreadItemAdapter<I extends ThreadItem>
|
public class ThreadItemAdapter<I extends ThreadItem>
|
||||||
extends RecyclerView.Adapter<ThreadItemViewHolder<I>>
|
extends RecyclerView.Adapter<BaseThreadItemViewHolder<I>>
|
||||||
implements VersionedAdapter {
|
implements VersionedAdapter {
|
||||||
|
|
||||||
static final int UNDEFINED = -1;
|
static final int UNDEFINED = -1;
|
||||||
@@ -42,7 +46,15 @@ public abstract class ThreadItemAdapter<I extends ThreadItem>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
I item = getVisibleItem(position);
|
||||||
if (item == null) return;
|
if (item == null) return;
|
||||||
listener.onItemVisible(item);
|
listener.onItemVisible(item);
|
||||||
@@ -304,7 +316,7 @@ public abstract class ThreadItemAdapter<I extends ThreadItem>
|
|||||||
revision++;
|
revision++;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected interface ThreadItemListener<I> {
|
public interface ThreadItemListener<I> {
|
||||||
|
|
||||||
void onItemVisible(I item);
|
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.GONE;
|
||||||
import static android.view.View.VISIBLE;
|
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
|
extends BriarActivity
|
||||||
implements ThreadListListener<H>, TextInputListener,
|
implements ThreadListListener<H>, TextInputListener,
|
||||||
ThreadItemListener<I> {
|
ThreadItemListener<I> {
|
||||||
@@ -46,7 +46,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
|||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
Logger.getLogger(ThreadListActivity.class.getName());
|
Logger.getLogger(ThreadListActivity.class.getName());
|
||||||
|
|
||||||
protected A adapter;
|
protected ThreadItemAdapter<I> adapter;
|
||||||
protected BriarRecyclerView list;
|
protected BriarRecyclerView list;
|
||||||
protected TextInputView textInput;
|
protected TextInputView textInput;
|
||||||
protected GroupId groupId;
|
protected GroupId groupId;
|
||||||
@@ -88,7 +88,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
|||||||
@LayoutRes
|
@LayoutRes
|
||||||
protected abstract int getLayout();
|
protected abstract int getLayout();
|
||||||
|
|
||||||
protected abstract A createAdapter(LinearLayoutManager layoutManager);
|
protected abstract ThreadItemAdapter<I> createAdapter(
|
||||||
|
LinearLayoutManager layoutManager);
|
||||||
|
|
||||||
protected void loadNamedGroup() {
|
protected void loadNamedGroup() {
|
||||||
getController().loadNamedGroup(
|
getController().loadNamedGroup(
|
||||||
@@ -249,8 +250,7 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
|
|||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
getController().createAndStoreMessage(text,
|
getController().createAndStoreMessage(text, replyItem, handler);
|
||||||
replyItem != null ? replyItem.getId() : null, handler);
|
|
||||||
textInput.hideSoftKeyboard();
|
textInput.hideSoftKeyboard();
|
||||||
textInput.setVisibility(GONE);
|
textInput.setVisibility(GONE);
|
||||||
textInput.setText("");
|
textInput.setText("");
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import org.briarproject.api.clients.NamedGroup;
|
|||||||
import org.briarproject.api.clients.PostHeader;
|
import org.briarproject.api.clients.PostHeader;
|
||||||
import org.briarproject.api.db.DbException;
|
import org.briarproject.api.db.DbException;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.briarproject.api.sync.MessageId;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
@@ -29,7 +28,7 @@ public interface ThreadListController<G extends NamedGroup, I extends ThreadItem
|
|||||||
|
|
||||||
void markItemsRead(Collection<I> items);
|
void markItemsRead(Collection<I> items);
|
||||||
|
|
||||||
void createAndStoreMessage(String body, @Nullable MessageId parentId,
|
void createAndStoreMessage(String body, @Nullable I parentItem,
|
||||||
ResultExceptionHandler<I, DbException> handler);
|
ResultExceptionHandler<I, DbException> handler);
|
||||||
|
|
||||||
void deleteNamedGroup(ResultExceptionHandler<Void, DbException> handler);
|
void deleteNamedGroup(ResultExceptionHandler<Void, DbException> handler);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package org.briarproject.android.threaded;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.support.annotation.CallSuper;
|
import android.support.annotation.CallSuper;
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.briarproject.android.api.AndroidNotificationManager;
|
import org.briarproject.android.api.AndroidNotificationManager;
|
||||||
import org.briarproject.android.controller.DbControllerImpl;
|
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.EventListener;
|
||||||
import org.briarproject.api.event.GroupRemovedEvent;
|
import org.briarproject.api.event.GroupRemovedEvent;
|
||||||
import org.briarproject.api.identity.IdentityManager;
|
import org.briarproject.api.identity.IdentityManager;
|
||||||
import org.briarproject.api.identity.LocalAuthor;
|
|
||||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.briarproject.api.sync.MessageId;
|
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 =
|
private static final Logger LOG =
|
||||||
Logger.getLogger(ThreadListControllerImpl.class.getName());
|
Logger.getLogger(ThreadListControllerImpl.class.getName());
|
||||||
|
|
||||||
private final IdentityManager identityManager;
|
protected final IdentityManager identityManager;
|
||||||
private final Executor cryptoExecutor;
|
protected final Executor cryptoExecutor;
|
||||||
protected final AndroidNotificationManager notificationManager;
|
protected final AndroidNotificationManager notificationManager;
|
||||||
|
protected final Clock clock;
|
||||||
private final EventBus eventBus;
|
private final EventBus eventBus;
|
||||||
private final Clock clock;
|
|
||||||
|
|
||||||
private final Map<MessageId, String> bodyCache = new ConcurrentHashMap<>();
|
private final Map<MessageId, String> bodyCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private volatile GroupId groupId;
|
private volatile GroupId groupId;
|
||||||
|
|
||||||
protected ThreadListListener<H> listener;
|
protected volatile ThreadListListener<H> listener;
|
||||||
|
|
||||||
protected ThreadListControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
protected ThreadListControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||||
LifecycleManager lifecycleManager, IdentityManager identityManager,
|
LifecycleManager lifecycleManager, IdentityManager identityManager,
|
||||||
@CryptoExecutor Executor cryptoExecutor, EventBus eventBus,
|
@CryptoExecutor Executor cryptoExecutor, EventBus eventBus,
|
||||||
AndroidNotificationManager notificationManager, Clock clock) {
|
Clock clock, AndroidNotificationManager notificationManager) {
|
||||||
super(dbExecutor, lifecycleManager);
|
super(dbExecutor, lifecycleManager);
|
||||||
this.identityManager = identityManager;
|
this.identityManager = identityManager;
|
||||||
this.cryptoExecutor = cryptoExecutor;
|
this.cryptoExecutor = cryptoExecutor;
|
||||||
this.eventBus = eventBus;
|
|
||||||
this.notificationManager = notificationManager;
|
this.notificationManager = notificationManager;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
|
this.eventBus = eventBus;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -160,7 +158,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
|
|||||||
for (H header : headers) {
|
for (H header : headers) {
|
||||||
if (!bodyCache.containsKey(header.getId())) {
|
if (!bodyCache.containsKey(header.getId())) {
|
||||||
bodyCache.put(header.getId(),
|
bodyCache.put(header.getId(),
|
||||||
loadMessageBody(header.getId()));
|
loadMessageBody(header));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
duration = System.currentTimeMillis() - now;
|
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;
|
protected abstract Collection<H> loadHeaders() throws DbException;
|
||||||
|
|
||||||
@DatabaseExecutor
|
@DatabaseExecutor
|
||||||
protected abstract String loadMessageBody(MessageId id) throws DbException;
|
protected abstract String loadMessageBody(H header) throws DbException;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void loadItem(final H header,
|
public void loadItem(final H header,
|
||||||
@@ -194,7 +192,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
|
|||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
String body;
|
String body;
|
||||||
if (!bodyCache.containsKey(header.getId())) {
|
if (!bodyCache.containsKey(header.getId())) {
|
||||||
body = loadMessageBody(header.getId());
|
body = loadMessageBody(header);
|
||||||
bodyCache.put(header.getId(), body);
|
bodyCache.put(header.getId(), body);
|
||||||
} else {
|
} else {
|
||||||
body = bodyCache.get(header.getId());
|
body = bodyCache.get(header.getId());
|
||||||
@@ -242,57 +240,7 @@ public abstract class ThreadListControllerImpl<G extends NamedGroup, I extends T
|
|||||||
@DatabaseExecutor
|
@DatabaseExecutor
|
||||||
protected abstract void markRead(MessageId id) throws DbException;
|
protected abstract void markRead(MessageId id) throws DbException;
|
||||||
|
|
||||||
@Override
|
protected void storePost(final M msg, final String body,
|
||||||
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,
|
|
||||||
final ResultExceptionHandler<I, DbException> resultHandler) {
|
final ResultExceptionHandler<I, DbException> resultHandler) {
|
||||||
runOnDbThread(new Runnable() {
|
runOnDbThread(new Runnable() {
|
||||||
@Override
|
@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.TestUtils;
|
||||||
import org.briarproject.android.TestBriarApplication;
|
import org.briarproject.android.TestBriarApplication;
|
||||||
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
||||||
|
import org.briarproject.android.threaded.ThreadItemAdapter;
|
||||||
import org.briarproject.api.db.DbException;
|
import org.briarproject.api.db.DbException;
|
||||||
import org.briarproject.api.identity.Author;
|
import org.briarproject.api.identity.Author;
|
||||||
import org.briarproject.api.identity.AuthorId;
|
import org.briarproject.api.identity.AuthorId;
|
||||||
@@ -111,7 +112,7 @@ public class ForumActivityTest {
|
|||||||
List<ForumItem> dummyData = getDummyData();
|
List<ForumItem> dummyData = getDummyData();
|
||||||
verify(mc, times(1)).loadItems(rc.capture());
|
verify(mc, times(1)).loadItems(rc.capture());
|
||||||
rc.getValue().onResult(dummyData);
|
rc.getValue().onResult(dummyData);
|
||||||
NestedForumAdapter adapter = forumActivity.getAdapter();
|
ThreadItemAdapter<ForumItem> adapter = forumActivity.getAdapter();
|
||||||
Assert.assertNotNull(adapter);
|
Assert.assertNotNull(adapter);
|
||||||
// Cascade close
|
// Cascade close
|
||||||
assertEquals(6, adapter.getItemCount());
|
assertEquals(6, adapter.getItemCount());
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.briarproject.android.forum;
|
|||||||
import org.briarproject.android.ActivityModule;
|
import org.briarproject.android.ActivityModule;
|
||||||
import org.briarproject.android.controller.BriarController;
|
import org.briarproject.android.controller.BriarController;
|
||||||
import org.briarproject.android.controller.BriarControllerImpl;
|
import org.briarproject.android.controller.BriarControllerImpl;
|
||||||
|
import org.briarproject.android.threaded.ThreadItemAdapter;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,7 +16,7 @@ public class TestForumActivity extends ForumActivity {
|
|||||||
return forumController;
|
return forumController;
|
||||||
}
|
}
|
||||||
|
|
||||||
public NestedForumAdapter getAdapter() {
|
public ThreadItemAdapter<ForumItem> getAdapter() {
|
||||||
return adapter;
|
return adapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package org.briarproject.api.clients;
|
|||||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.api.sync.Message;
|
import org.briarproject.api.sync.Message;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import javax.annotation.concurrent.Immutable;
|
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.DbException;
|
||||||
import org.briarproject.api.db.Transaction;
|
import org.briarproject.api.db.Transaction;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
|
import org.briarproject.api.sync.InvalidMessageException;
|
||||||
import org.briarproject.api.sync.Message;
|
import org.briarproject.api.sync.Message;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
|
|
||||||
@@ -81,4 +82,8 @@ public interface ClientHelper {
|
|||||||
|
|
||||||
byte[] sign(BdfList toSign, byte[] privateKey)
|
byte[] sign(BdfList toSign, byte[] privateKey)
|
||||||
throws FormatException, GeneralSecurityException;
|
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.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.api.sync.Message;
|
import org.briarproject.api.sync.Message;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import javax.annotation.concurrent.Immutable;
|
import javax.annotation.concurrent.Immutable;
|
||||||
@@ -14,16 +13,16 @@ import javax.annotation.concurrent.Immutable;
|
|||||||
@NotNullByDefault
|
@NotNullByDefault
|
||||||
public class GroupMessage extends BaseMessage {
|
public class GroupMessage extends BaseMessage {
|
||||||
|
|
||||||
private final Author author;
|
private final Author member;
|
||||||
|
|
||||||
public GroupMessage(Message message, @Nullable MessageId parent,
|
public GroupMessage(Message message, @Nullable MessageId parent,
|
||||||
Author author) {
|
Author member) {
|
||||||
super(message, parent);
|
super(message, parent);
|
||||||
this.author = author;
|
this.member = member;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Author getAuthor() {
|
public Author getMember() {
|
||||||
return author;
|
return member;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,58 @@
|
|||||||
package org.briarproject.api.privategroup;
|
package org.briarproject.api.privategroup;
|
||||||
|
|
||||||
import org.briarproject.api.FormatException;
|
import org.briarproject.api.crypto.CryptoExecutor;
|
||||||
import org.briarproject.api.crypto.PrivateKey;
|
|
||||||
import org.briarproject.api.identity.Author;
|
import org.briarproject.api.identity.Author;
|
||||||
import org.briarproject.api.identity.LocalAuthor;
|
import org.briarproject.api.identity.LocalAuthor;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.security.GeneralSecurityException;
|
|
||||||
|
|
||||||
public interface GroupMessageFactory {
|
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,
|
GroupMessage createGroupMessage(GroupId groupId, long timestamp,
|
||||||
MessageId parent, LocalAuthor author, String body)
|
@Nullable MessageId parentId, LocalAuthor author, String body,
|
||||||
throws FormatException, GeneralSecurityException;
|
MessageId previousMsgId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,19 +3,23 @@ package org.briarproject.api.privategroup;
|
|||||||
import org.briarproject.api.clients.PostHeader;
|
import org.briarproject.api.clients.PostHeader;
|
||||||
import org.briarproject.api.identity.Author;
|
import org.briarproject.api.identity.Author;
|
||||||
import org.briarproject.api.identity.Author.Status;
|
import org.briarproject.api.identity.Author.Status;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@NotNullByDefault
|
||||||
public class GroupMessageHeader extends PostHeader {
|
public class GroupMessageHeader extends PostHeader {
|
||||||
|
|
||||||
private final GroupId groupId;
|
private final GroupId groupId;
|
||||||
|
|
||||||
public GroupMessageHeader(@NotNull GroupId groupId, @NotNull MessageId id,
|
public GroupMessageHeader(GroupId groupId, MessageId id,
|
||||||
@Nullable MessageId parentId, long timestamp,
|
@Nullable MessageId parentId, long timestamp,
|
||||||
@NotNull Author author, @NotNull Status authorStatus,
|
Author author, Status authorStatus, boolean read) {
|
||||||
boolean read) {
|
|
||||||
super(id, parentId, timestamp, author, authorStatus, read);
|
super(id, parentId, timestamp, author, authorStatus, read);
|
||||||
this.groupId = groupId;
|
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.clients.MessageTracker;
|
||||||
import org.briarproject.api.db.DbException;
|
import org.briarproject.api.db.DbException;
|
||||||
import org.briarproject.api.db.Transaction;
|
import org.briarproject.api.db.Transaction;
|
||||||
import org.briarproject.api.identity.LocalAuthor;
|
|
||||||
import org.briarproject.api.sync.ClientId;
|
import org.briarproject.api.sync.ClientId;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
public interface PrivateGroupManager extends MessageTracker {
|
public interface PrivateGroupManager extends MessageTracker {
|
||||||
|
|
||||||
/** Returns the unique ID of the private group client. */
|
/** Returns the unique ID of the private group client. */
|
||||||
@NotNull
|
|
||||||
ClientId getClientId();
|
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. */
|
/** Removes a dissolved private group. */
|
||||||
void removePrivateGroup(GroupId g) throws DbException;
|
void removePrivateGroup(GroupId g) throws DbException;
|
||||||
|
|
||||||
/** Creates a local group message. */
|
/** Gets the MessageId of your previous message sent to the group */
|
||||||
GroupMessage createLocalMessage(GroupId groupId, String body,
|
MessageId getPreviousMsgId(GroupId g) throws DbException;
|
||||||
long timestamp, @Nullable MessageId parentId, LocalAuthor author);
|
|
||||||
|
/** 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. */
|
/** Stores (and sends) a local group message. */
|
||||||
GroupMessageHeader addLocalMessage(GroupMessage p) throws DbException;
|
GroupMessageHeader addLocalMessage(GroupMessage p) throws DbException;
|
||||||
|
|
||||||
/** Returns the private group with the given ID. */
|
/** Returns the private group with the given ID. */
|
||||||
@NotNull
|
|
||||||
PrivateGroup getPrivateGroup(GroupId g) throws DbException;
|
PrivateGroup getPrivateGroup(GroupId g) throws DbException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the private group with the given ID within the given transaction.
|
* Returns the private group with the given ID within the given transaction.
|
||||||
*/
|
*/
|
||||||
@NotNull
|
|
||||||
PrivateGroup getPrivateGroup(Transaction txn, GroupId g) throws DbException;
|
PrivateGroup getPrivateGroup(Transaction txn, GroupId g) throws DbException;
|
||||||
|
|
||||||
/** Returns all private groups the user is a member of. */
|
/** Returns all private groups the user is a member of. */
|
||||||
@NotNull
|
|
||||||
Collection<PrivateGroup> getPrivateGroups() throws DbException;
|
Collection<PrivateGroup> getPrivateGroups() throws DbException;
|
||||||
|
|
||||||
/** Returns true if the private group has been dissolved. */
|
/** Returns true if the private group has been dissolved. */
|
||||||
boolean isDissolved(GroupId g) throws DbException;
|
boolean isDissolved(GroupId g) throws DbException;
|
||||||
|
|
||||||
/** Returns the body of the group message with the given ID. */
|
/** Returns the body of the group message with the given ID. */
|
||||||
@NotNull
|
|
||||||
String getMessageBody(MessageId m) throws DbException;
|
String getMessageBody(MessageId m) throws DbException;
|
||||||
|
|
||||||
/** Returns the headers of all group messages in the given group. */
|
/** Returns the headers of all group messages in the given group. */
|
||||||
@NotNull
|
|
||||||
Collection<GroupMessageHeader> getHeaders(GroupId g) throws DbException;
|
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.blogs.MessageType;
|
||||||
import org.briarproject.api.clients.BdfMessageContext;
|
import org.briarproject.api.clients.BdfMessageContext;
|
||||||
import org.briarproject.api.clients.ClientHelper;
|
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.BdfDictionary;
|
||||||
import org.briarproject.api.data.BdfEntry;
|
import org.briarproject.api.data.BdfEntry;
|
||||||
import org.briarproject.api.data.BdfList;
|
import org.briarproject.api.data.BdfList;
|
||||||
@@ -48,18 +44,15 @@ import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH
|
|||||||
|
|
||||||
class BlogPostValidator extends BdfMessageValidator {
|
class BlogPostValidator extends BdfMessageValidator {
|
||||||
|
|
||||||
private final CryptoComponent crypto;
|
|
||||||
private final GroupFactory groupFactory;
|
private final GroupFactory groupFactory;
|
||||||
private final MessageFactory messageFactory;
|
private final MessageFactory messageFactory;
|
||||||
private final BlogFactory blogFactory;
|
private final BlogFactory blogFactory;
|
||||||
|
|
||||||
BlogPostValidator(CryptoComponent crypto, GroupFactory groupFactory,
|
BlogPostValidator(GroupFactory groupFactory, MessageFactory messageFactory,
|
||||||
MessageFactory messageFactory, BlogFactory blogFactory,
|
BlogFactory blogFactory, ClientHelper clientHelper,
|
||||||
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
MetadataEncoder metadataEncoder, Clock clock) {
|
||||||
Clock clock) {
|
|
||||||
super(clientHelper, metadataEncoder, clock);
|
super(clientHelper, metadataEncoder, clock);
|
||||||
|
|
||||||
this.crypto = crypto;
|
|
||||||
this.groupFactory = groupFactory;
|
this.groupFactory = groupFactory;
|
||||||
this.messageFactory = messageFactory;
|
this.messageFactory = messageFactory;
|
||||||
this.blogFactory = blogFactory;
|
this.blogFactory = blogFactory;
|
||||||
@@ -109,7 +102,11 @@ class BlogPostValidator extends BdfMessageValidator {
|
|||||||
BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), postBody);
|
BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), postBody);
|
||||||
Blog b = blogFactory.parseBlog(g, ""); // description doesn't matter
|
Blog b = blogFactory.parseBlog(g, ""); // description doesn't matter
|
||||||
Author a = b.getAuthor();
|
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
|
// Return the metadata and dependencies
|
||||||
BdfDictionary meta = new BdfDictionary();
|
BdfDictionary meta = new BdfDictionary();
|
||||||
@@ -150,7 +147,11 @@ class BlogPostValidator extends BdfMessageValidator {
|
|||||||
currentId);
|
currentId);
|
||||||
Blog b = blogFactory.parseBlog(g, ""); // description doesn't matter
|
Blog b = blogFactory.parseBlog(g, ""); // description doesn't matter
|
||||||
Author a = b.getAuthor();
|
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
|
// Return the metadata and dependencies
|
||||||
BdfDictionary meta = new BdfDictionary();
|
BdfDictionary meta = new BdfDictionary();
|
||||||
@@ -267,26 +268,6 @@ class BlogPostValidator extends BdfMessageValidator {
|
|||||||
return new BdfMessageContext(meta, dependencies);
|
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) {
|
static BdfDictionary authorToBdfDictionary(Author a) {
|
||||||
return BdfDictionary.of(
|
return BdfDictionary.of(
|
||||||
new BdfEntry(KEY_AUTHOR_ID, a.getId()),
|
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.blogs.BlogPostFactory;
|
||||||
import org.briarproject.api.clients.ClientHelper;
|
import org.briarproject.api.clients.ClientHelper;
|
||||||
import org.briarproject.api.contact.ContactManager;
|
import org.briarproject.api.contact.ContactManager;
|
||||||
import org.briarproject.api.crypto.CryptoComponent;
|
|
||||||
import org.briarproject.api.data.MetadataEncoder;
|
import org.briarproject.api.data.MetadataEncoder;
|
||||||
import org.briarproject.api.identity.AuthorFactory;
|
import org.briarproject.api.identity.AuthorFactory;
|
||||||
import org.briarproject.api.identity.IdentityManager;
|
import org.briarproject.api.identity.IdentityManager;
|
||||||
@@ -64,14 +63,14 @@ public class BlogsModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
BlogPostValidator provideBlogPostValidator(
|
BlogPostValidator provideBlogPostValidator(
|
||||||
ValidationManager validationManager, CryptoComponent crypto,
|
ValidationManager validationManager, GroupFactory groupFactory,
|
||||||
GroupFactory groupFactory, MessageFactory messageFactory,
|
MessageFactory messageFactory, BlogFactory blogFactory,
|
||||||
BlogFactory blogFactory, ClientHelper clientHelper,
|
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
||||||
MetadataEncoder metadataEncoder, Clock clock) {
|
Clock clock) {
|
||||||
|
|
||||||
BlogPostValidator validator = new BlogPostValidator(crypto,
|
BlogPostValidator validator = new BlogPostValidator(groupFactory,
|
||||||
groupFactory, messageFactory, blogFactory, clientHelper,
|
messageFactory, blogFactory, clientHelper, metadataEncoder,
|
||||||
metadataEncoder, clock);
|
clock);
|
||||||
validationManager.registerMessageValidator(CLIENT_ID, validator);
|
validationManager.registerMessageValidator(CLIENT_ID, validator);
|
||||||
|
|
||||||
return validator;
|
return validator;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import org.briarproject.api.clients.ClientHelper;
|
|||||||
import org.briarproject.api.crypto.CryptoComponent;
|
import org.briarproject.api.crypto.CryptoComponent;
|
||||||
import org.briarproject.api.crypto.KeyParser;
|
import org.briarproject.api.crypto.KeyParser;
|
||||||
import org.briarproject.api.crypto.PrivateKey;
|
import org.briarproject.api.crypto.PrivateKey;
|
||||||
|
import org.briarproject.api.crypto.PublicKey;
|
||||||
import org.briarproject.api.crypto.Signature;
|
import org.briarproject.api.crypto.Signature;
|
||||||
import org.briarproject.api.data.BdfDictionary;
|
import org.briarproject.api.data.BdfDictionary;
|
||||||
import org.briarproject.api.data.BdfList;
|
import org.briarproject.api.data.BdfList;
|
||||||
@@ -320,4 +321,20 @@ class ClientHelperImpl implements ClientHelper {
|
|||||||
signature.update(toByteArray(toSign));
|
signature.update(toByteArray(toSign));
|
||||||
return signature.sign();
|
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
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
ForumPostValidator provideForumPostValidator(
|
ForumPostValidator provideForumPostValidator(
|
||||||
ValidationManager validationManager, CryptoComponent crypto,
|
ValidationManager validationManager, AuthorFactory authorFactory,
|
||||||
AuthorFactory authorFactory, ClientHelper clientHelper,
|
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
||||||
MetadataEncoder metadataEncoder, Clock clock) {
|
Clock clock) {
|
||||||
ForumPostValidator validator = new ForumPostValidator(crypto,
|
ForumPostValidator validator = new ForumPostValidator(authorFactory,
|
||||||
authorFactory, clientHelper, metadataEncoder, clock);
|
clientHelper, metadataEncoder, clock);
|
||||||
validationManager.registerMessageValidator(
|
validationManager.registerMessageValidator(
|
||||||
ForumManagerImpl.CLIENT_ID, validator);
|
ForumManagerImpl.CLIENT_ID, validator);
|
||||||
return validator;
|
return validator;
|
||||||
|
|||||||
@@ -4,10 +4,6 @@ import org.briarproject.api.FormatException;
|
|||||||
import org.briarproject.api.UniqueId;
|
import org.briarproject.api.UniqueId;
|
||||||
import org.briarproject.api.clients.BdfMessageContext;
|
import org.briarproject.api.clients.BdfMessageContext;
|
||||||
import org.briarproject.api.clients.ClientHelper;
|
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.BdfDictionary;
|
||||||
import org.briarproject.api.data.BdfList;
|
import org.briarproject.api.data.BdfList;
|
||||||
import org.briarproject.api.data.MetadataEncoder;
|
import org.briarproject.api.data.MetadataEncoder;
|
||||||
@@ -32,14 +28,11 @@ import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH
|
|||||||
|
|
||||||
class ForumPostValidator extends BdfMessageValidator {
|
class ForumPostValidator extends BdfMessageValidator {
|
||||||
|
|
||||||
private final CryptoComponent crypto;
|
|
||||||
private final AuthorFactory authorFactory;
|
private final AuthorFactory authorFactory;
|
||||||
|
|
||||||
ForumPostValidator(CryptoComponent crypto, AuthorFactory authorFactory,
|
ForumPostValidator(AuthorFactory authorFactory, ClientHelper clientHelper,
|
||||||
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
MetadataEncoder metadataEncoder, Clock clock) {
|
||||||
Clock clock) {
|
|
||||||
super(clientHelper, metadataEncoder, clock);
|
super(clientHelper, metadataEncoder, clock);
|
||||||
this.crypto = crypto;
|
|
||||||
this.authorFactory = authorFactory;
|
this.authorFactory = authorFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,22 +74,14 @@ class ForumPostValidator extends BdfMessageValidator {
|
|||||||
}
|
}
|
||||||
// Verify the signature, if any
|
// Verify the signature, if any
|
||||||
if (author != null) {
|
if (author != null) {
|
||||||
|
// Serialise the data to be verified
|
||||||
|
BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), parent,
|
||||||
|
authorList, contentType, forumPostBody);
|
||||||
try {
|
try {
|
||||||
// Parse the public key
|
clientHelper
|
||||||
KeyParser keyParser = crypto.getSignatureKeyParser();
|
.verifySignature(sig, author.getPublicKey(), signed);
|
||||||
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");
|
|
||||||
}
|
|
||||||
} catch (GeneralSecurityException e) {
|
} catch (GeneralSecurityException e) {
|
||||||
throw new InvalidMessageException("Invalid public key");
|
throw new InvalidMessageException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Return the metadata and dependencies
|
// Return the metadata and dependencies
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
package org.briarproject.privategroup;
|
package org.briarproject.privategroup;
|
||||||
|
|
||||||
|
import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
|
||||||
|
|
||||||
interface Constants {
|
interface Constants {
|
||||||
|
|
||||||
// Database keys
|
// 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.FormatException;
|
||||||
import org.briarproject.api.clients.ClientHelper;
|
import org.briarproject.api.clients.ClientHelper;
|
||||||
import org.briarproject.api.data.BdfList;
|
import org.briarproject.api.data.BdfList;
|
||||||
|
import org.briarproject.api.identity.Author;
|
||||||
import org.briarproject.api.identity.LocalAuthor;
|
import org.briarproject.api.identity.LocalAuthor;
|
||||||
|
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.api.privategroup.GroupMessage;
|
import org.briarproject.api.privategroup.GroupMessage;
|
||||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.briarproject.api.sync.Message;
|
import org.briarproject.api.sync.Message;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
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 {
|
class GroupMessageFactoryImpl implements GroupMessageFactory {
|
||||||
|
|
||||||
private final ClientHelper clientHelper;
|
private final ClientHelper clientHelper;
|
||||||
@@ -24,20 +31,82 @@ class GroupMessageFactoryImpl implements GroupMessageFactory {
|
|||||||
this.clientHelper = clientHelper;
|
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
|
@Override
|
||||||
public GroupMessage createGroupMessage(GroupId groupId, long timestamp,
|
public GroupMessage createGroupMessage(GroupId groupId, long timestamp,
|
||||||
MessageId parent, LocalAuthor author, String body)
|
@Nullable MessageId parentId, LocalAuthor author, String content,
|
||||||
throws FormatException, GeneralSecurityException {
|
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
|
// Compose the message
|
||||||
byte[] sig = clientHelper.sign(new BdfList(), author.getPrivateKey());
|
BdfList body =
|
||||||
|
BdfList.of(type, author.getName(),
|
||||||
|
author.getPublicKey(), parentId, previousMsgId,
|
||||||
|
content, signature);
|
||||||
|
Message m = clientHelper.createMessage(groupId, timestamp, body);
|
||||||
|
|
||||||
// Compose the message
|
return new GroupMessage(m, parentId, author);
|
||||||
Message m =
|
} catch (GeneralSecurityException e) {
|
||||||
clientHelper.createMessage(groupId, timestamp, new BdfList());
|
throw new RuntimeException(e);
|
||||||
|
} catch (FormatException e) {
|
||||||
return new GroupMessage(m, parent, author);
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ package org.briarproject.privategroup;
|
|||||||
import org.briarproject.api.FormatException;
|
import org.briarproject.api.FormatException;
|
||||||
import org.briarproject.api.clients.BdfMessageContext;
|
import org.briarproject.api.clients.BdfMessageContext;
|
||||||
import org.briarproject.api.clients.ClientHelper;
|
import org.briarproject.api.clients.ClientHelper;
|
||||||
import org.briarproject.api.crypto.CryptoComponent;
|
|
||||||
import org.briarproject.api.data.BdfDictionary;
|
import org.briarproject.api.data.BdfDictionary;
|
||||||
import org.briarproject.api.data.BdfList;
|
import org.briarproject.api.data.BdfList;
|
||||||
import org.briarproject.api.data.MetadataEncoder;
|
import org.briarproject.api.data.MetadataEncoder;
|
||||||
|
import org.briarproject.api.identity.Author;
|
||||||
import org.briarproject.api.identity.AuthorFactory;
|
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.Group;
|
||||||
import org.briarproject.api.sync.InvalidMessageException;
|
import org.briarproject.api.sync.InvalidMessageException;
|
||||||
import org.briarproject.api.sync.Message;
|
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.api.system.Clock;
|
||||||
import org.briarproject.clients.BdfMessageValidator;
|
import org.briarproject.clients.BdfMessageValidator;
|
||||||
|
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
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 {
|
class GroupMessageValidator extends BdfMessageValidator {
|
||||||
|
|
||||||
private final CryptoComponent crypto;
|
private final PrivateGroupFactory groupFactory;
|
||||||
private final AuthorFactory authorFactory;
|
private final AuthorFactory authorFactory;
|
||||||
|
|
||||||
GroupMessageValidator(CryptoComponent crypto, AuthorFactory authorFactory,
|
GroupMessageValidator(PrivateGroupFactory groupFactory,
|
||||||
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
||||||
Clock clock) {
|
Clock clock, AuthorFactory authorFactory) {
|
||||||
super(clientHelper, metadataEncoder, clock);
|
super(clientHelper, metadataEncoder, clock);
|
||||||
this.crypto = crypto;
|
this.groupFactory = groupFactory;
|
||||||
this.authorFactory = authorFactory;
|
this.authorFactory = authorFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,9 +57,168 @@ class GroupMessageValidator extends BdfMessageValidator {
|
|||||||
protected BdfMessageContext validateMessage(Message m, Group g,
|
protected BdfMessageContext validateMessage(Message m, Group g,
|
||||||
BdfList body) throws InvalidMessageException, FormatException {
|
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();
|
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);
|
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.FormatException;
|
||||||
import org.briarproject.api.clients.ClientHelper;
|
import org.briarproject.api.clients.ClientHelper;
|
||||||
import org.briarproject.api.data.BdfDictionary;
|
import org.briarproject.api.data.BdfDictionary;
|
||||||
|
import org.briarproject.api.data.BdfEntry;
|
||||||
import org.briarproject.api.data.BdfList;
|
import org.briarproject.api.data.BdfList;
|
||||||
import org.briarproject.api.data.MetadataParser;
|
import org.briarproject.api.data.MetadataParser;
|
||||||
import org.briarproject.api.db.DatabaseComponent;
|
import org.briarproject.api.db.DatabaseComponent;
|
||||||
import org.briarproject.api.db.DbException;
|
import org.briarproject.api.db.DbException;
|
||||||
import org.briarproject.api.db.Transaction;
|
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.IdentityManager;
|
||||||
import org.briarproject.api.identity.LocalAuthor;
|
|
||||||
import org.briarproject.api.privategroup.GroupMessage;
|
import org.briarproject.api.privategroup.GroupMessage;
|
||||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
|
||||||
import org.briarproject.api.privategroup.GroupMessageHeader;
|
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.PrivateGroup;
|
||||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
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.GroupId;
|
||||||
import org.briarproject.api.sync.Message;
|
import org.briarproject.api.sync.Message;
|
||||||
import org.briarproject.api.sync.MessageId;
|
import org.briarproject.api.sync.MessageId;
|
||||||
import org.briarproject.api.system.Clock;
|
|
||||||
import org.briarproject.clients.BdfIncomingMessageHook;
|
import org.briarproject.clients.BdfIncomingMessageHook;
|
||||||
import org.briarproject.util.StringUtils;
|
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.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
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 java.util.logging.Logger;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import static org.briarproject.api.identity.Author.Status.OURSELVES;
|
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
|
public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
||||||
PrivateGroupManager {
|
PrivateGroupManager {
|
||||||
@@ -46,62 +63,102 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
|||||||
StringUtils.fromHexString("5072697661746547726f75704d616e61"
|
StringUtils.fromHexString("5072697661746547726f75704d616e61"
|
||||||
+ "67657220627920546f727374656e2047"));
|
+ "67657220627920546f727374656e2047"));
|
||||||
|
|
||||||
private final IdentityManager identityManager;
|
|
||||||
private final PrivateGroupFactory privateGroupFactory;
|
private final PrivateGroupFactory privateGroupFactory;
|
||||||
private final GroupMessageFactory groupMessageFactory;
|
private final IdentityManager identityManager;
|
||||||
private final Clock clock;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
PrivateGroupManagerImpl(ClientHelper clientHelper,
|
PrivateGroupManagerImpl(ClientHelper clientHelper,
|
||||||
MetadataParser metadataParser, DatabaseComponent db,
|
MetadataParser metadataParser, DatabaseComponent db,
|
||||||
IdentityManager identityManager,
|
|
||||||
PrivateGroupFactory privateGroupFactory,
|
PrivateGroupFactory privateGroupFactory,
|
||||||
GroupMessageFactory groupMessageFactory, Clock clock) {
|
IdentityManager identityManager) {
|
||||||
super(db, clientHelper, metadataParser);
|
super(db, clientHelper, metadataParser);
|
||||||
|
|
||||||
this.identityManager = identityManager;
|
|
||||||
this.privateGroupFactory = privateGroupFactory;
|
this.privateGroupFactory = privateGroupFactory;
|
||||||
this.groupMessageFactory = groupMessageFactory;
|
this.identityManager = identityManager;
|
||||||
this.clock = clock;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
@Override
|
||||||
public ClientId getClientId() {
|
public ClientId getClientId() {
|
||||||
return CLIENT_ID;
|
return CLIENT_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GroupId addPrivateGroup(String name) throws DbException {
|
public void addPrivateGroup(PrivateGroup group,
|
||||||
PrivateGroup group;
|
GroupMessage newMemberMsg, GroupMessage joinMsg)
|
||||||
|
throws DbException {
|
||||||
Transaction txn = db.startTransaction(false);
|
Transaction txn = db.startTransaction(false);
|
||||||
try {
|
try {
|
||||||
LocalAuthor a = identityManager.getLocalAuthor(txn);
|
|
||||||
group = privateGroupFactory.createPrivateGroup(name, a);
|
|
||||||
db.addGroup(txn, group.getGroup());
|
db.addGroup(txn, group.getGroup());
|
||||||
|
announceNewMember(txn, newMemberMsg);
|
||||||
|
joinPrivateGroup(txn, joinMsg);
|
||||||
txn.setComplete();
|
txn.setComplete();
|
||||||
|
} catch (FormatException e) {
|
||||||
|
throw new DbException(e);
|
||||||
} finally {
|
} finally {
|
||||||
db.endTransaction(txn);
|
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
|
@Override
|
||||||
public void removePrivateGroup(GroupId g) throws DbException {
|
public void removePrivateGroup(GroupId g) throws DbException {
|
||||||
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GroupMessage createLocalMessage(GroupId groupId, String body,
|
public MessageId getPreviousMsgId(GroupId g) throws DbException {
|
||||||
long timestamp, @Nullable MessageId parentId, LocalAuthor author) {
|
MessageId previousMsgId;
|
||||||
|
Transaction txn = db.startTransaction(true);
|
||||||
try {
|
try {
|
||||||
return groupMessageFactory
|
previousMsgId = getPreviousMsgId(txn, g);
|
||||||
.createGroupMessage(groupId, timestamp, parentId, author,
|
txn.setComplete();
|
||||||
body);
|
|
||||||
} catch (FormatException e) {
|
} catch (FormatException e) {
|
||||||
throw new RuntimeException(e);
|
throw new DbException(e);
|
||||||
} catch (GeneralSecurityException e) {
|
} finally {
|
||||||
throw new RuntimeException(e);
|
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);
|
Transaction txn = db.startTransaction(false);
|
||||||
try {
|
try {
|
||||||
BdfDictionary meta = new BdfDictionary();
|
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);
|
clientHelper.addLocalMessage(txn, m.getMessage(), meta, true);
|
||||||
|
setPreviousMsgId(txn, m.getMessage().getGroupId(),
|
||||||
|
m.getMessage().getId());
|
||||||
trackOutgoingMessage(txn, m.getMessage());
|
trackOutgoingMessage(txn, m.getMessage());
|
||||||
txn.setComplete();
|
txn.setComplete();
|
||||||
} catch (FormatException e) {
|
} catch (FormatException e) {
|
||||||
@@ -121,10 +183,18 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
|||||||
}
|
}
|
||||||
return new GroupMessageHeader(m.getMessage().getGroupId(),
|
return new GroupMessageHeader(m.getMessage().getGroupId(),
|
||||||
m.getMessage().getId(), m.getParent(),
|
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
|
@Override
|
||||||
public PrivateGroup getPrivateGroup(GroupId g) throws DbException {
|
public PrivateGroup getPrivateGroup(GroupId g) throws DbException {
|
||||||
PrivateGroup privateGroup;
|
PrivateGroup privateGroup;
|
||||||
@@ -138,7 +208,6 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
|||||||
return privateGroup;
|
return privateGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
@Override
|
||||||
public PrivateGroup getPrivateGroup(Transaction txn, GroupId g)
|
public PrivateGroup getPrivateGroup(Transaction txn, GroupId g)
|
||||||
throws DbException {
|
throws DbException {
|
||||||
@@ -150,7 +219,6 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<PrivateGroup> getPrivateGroups() throws DbException {
|
public Collection<PrivateGroup> getPrivateGroups() throws DbException {
|
||||||
Collection<Group> groups;
|
Collection<Group> groups;
|
||||||
@@ -178,27 +246,179 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
|
||||||
@Override
|
@Override
|
||||||
public String getMessageBody(MessageId m) throws DbException {
|
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
|
@Override
|
||||||
public Collection<GroupMessageHeader> getHeaders(GroupId g)
|
public Collection<GroupMessageHeader> getHeaders(GroupId g)
|
||||||
throws DbException {
|
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
|
@Override
|
||||||
protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
|
protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
|
||||||
BdfDictionary meta) throws DbException, FormatException {
|
BdfDictionary meta) throws DbException, FormatException {
|
||||||
|
|
||||||
trackIncomingMessage(txn, m);
|
long timestamp = meta.getLong(KEY_TIMESTAMP);
|
||||||
|
MessageType type =
|
||||||
return true;
|
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.clients.ClientHelper;
|
||||||
import org.briarproject.api.contact.ContactManager;
|
import org.briarproject.api.contact.ContactManager;
|
||||||
import org.briarproject.api.crypto.CryptoComponent;
|
|
||||||
import org.briarproject.api.data.MetadataEncoder;
|
import org.briarproject.api.data.MetadataEncoder;
|
||||||
import org.briarproject.api.identity.AuthorFactory;
|
import org.briarproject.api.identity.AuthorFactory;
|
||||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||||
@@ -59,13 +58,17 @@ public class PrivateGroupModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
GroupMessageValidator provideGroupMessageValidator(
|
GroupMessageValidator provideGroupMessageValidator(
|
||||||
ValidationManager validationManager, CryptoComponent crypto,
|
PrivateGroupFactory groupFactory,
|
||||||
AuthorFactory authorFactory, ClientHelper clientHelper,
|
ValidationManager validationManager, ClientHelper clientHelper,
|
||||||
MetadataEncoder metadataEncoder, Clock clock) {
|
MetadataEncoder metadataEncoder, Clock clock,
|
||||||
GroupMessageValidator validator = new GroupMessageValidator(crypto,
|
AuthorFactory authorFactory) {
|
||||||
authorFactory, clientHelper, metadataEncoder, clock);
|
|
||||||
|
GroupMessageValidator validator = new GroupMessageValidator(
|
||||||
|
groupFactory, clientHelper, metadataEncoder, clock,
|
||||||
|
authorFactory);
|
||||||
validationManager.registerMessageValidator(
|
validationManager.registerMessageValidator(
|
||||||
PrivateGroupManagerImpl.CLIENT_ID, validator);
|
PrivateGroupManagerImpl.CLIENT_ID, validator);
|
||||||
|
|
||||||
return validator;
|
return validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ import org.briarproject.api.blogs.Blog;
|
|||||||
import org.briarproject.api.blogs.BlogFactory;
|
import org.briarproject.api.blogs.BlogFactory;
|
||||||
import org.briarproject.api.clients.ClientHelper;
|
import org.briarproject.api.clients.ClientHelper;
|
||||||
import org.briarproject.api.crypto.CryptoComponent;
|
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.BdfDictionary;
|
||||||
import org.briarproject.api.data.BdfEntry;
|
import org.briarproject.api.data.BdfEntry;
|
||||||
import org.briarproject.api.data.BdfList;
|
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.Group;
|
||||||
import org.briarproject.api.sync.GroupFactory;
|
import org.briarproject.api.sync.GroupFactory;
|
||||||
import org.briarproject.api.sync.GroupId;
|
import org.briarproject.api.sync.GroupId;
|
||||||
import org.briarproject.api.sync.InvalidMessageException;
|
|
||||||
import org.briarproject.api.sync.Message;
|
import org.briarproject.api.sync.Message;
|
||||||
import org.briarproject.api.sync.MessageFactory;
|
import org.briarproject.api.sync.MessageFactory;
|
||||||
import org.briarproject.api.sync.MessageId;
|
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_ID;
|
||||||
import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR_NAME;
|
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_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_ORIGINAL_PARENT_MSG_ID;
|
||||||
import static org.briarproject.api.blogs.BlogConstants.KEY_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_PUBLIC_KEY;
|
||||||
import static org.briarproject.api.blogs.BlogConstants.KEY_READ;
|
import static org.briarproject.api.blogs.BlogConstants.KEY_READ;
|
||||||
import static org.briarproject.api.blogs.MessageType.COMMENT;
|
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);
|
message = new Message(messageId, group.getId(), timestamp, raw);
|
||||||
|
|
||||||
MetadataEncoder metadataEncoder = context.mock(MetadataEncoder.class);
|
MetadataEncoder metadataEncoder = context.mock(MetadataEncoder.class);
|
||||||
validator = new BlogPostValidator(cryptoComponent, groupFactory,
|
validator = new BlogPostValidator(groupFactory, messageFactory,
|
||||||
messageFactory, blogFactory, clientHelper, metadataEncoder,
|
blogFactory, clientHelper, metadataEncoder, clock);
|
||||||
clock);
|
|
||||||
context.assertIsSatisfied();
|
context.assertIsSatisfied();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +103,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
|
|||||||
|
|
||||||
BdfList signed =
|
BdfList signed =
|
||||||
BdfList.of(blog.getId(), message.getTimestamp(), body);
|
BdfList.of(blog.getId(), message.getTimestamp(), body);
|
||||||
expectCrypto(signed, sigBytes, true);
|
expectCrypto(signed, sigBytes);
|
||||||
final BdfDictionary result =
|
final BdfDictionary result =
|
||||||
validator.validateMessage(message, group, m).getDictionary();
|
validator.validateMessage(message, group, m).getDictionary();
|
||||||
|
|
||||||
@@ -135,18 +130,6 @@ public class BlogPostValidatorTest extends BriarTestCase {
|
|||||||
validator.validateMessage(message, group, m).getDictionary();
|
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
|
@Test
|
||||||
public void testValidateProperBlogComment()
|
public void testValidateProperBlogComment()
|
||||||
throws IOException, GeneralSecurityException {
|
throws IOException, GeneralSecurityException {
|
||||||
@@ -162,7 +145,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
|
|||||||
BdfList signed =
|
BdfList signed =
|
||||||
BdfList.of(blog.getId(), message.getTimestamp(), comment,
|
BdfList.of(blog.getId(), message.getTimestamp(), comment,
|
||||||
pOriginalId, currentId);
|
pOriginalId, currentId);
|
||||||
expectCrypto(signed, sigBytes, true);
|
expectCrypto(signed, sigBytes);
|
||||||
final BdfDictionary result =
|
final BdfDictionary result =
|
||||||
validator.validateMessage(message, group, m).getDictionary();
|
validator.validateMessage(message, group, m).getDictionary();
|
||||||
|
|
||||||
@@ -189,7 +172,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
|
|||||||
BdfList signed =
|
BdfList signed =
|
||||||
BdfList.of(blog.getId(), message.getTimestamp(), null,
|
BdfList.of(blog.getId(), message.getTimestamp(), null,
|
||||||
originalId, currentId);
|
originalId, currentId);
|
||||||
expectCrypto(signed, sigBytes, true);
|
expectCrypto(signed, sigBytes);
|
||||||
final BdfDictionary result =
|
final BdfDictionary result =
|
||||||
validator.validateMessage(message, group, m).getDictionary();
|
validator.validateMessage(message, group, m).getDictionary();
|
||||||
|
|
||||||
@@ -208,7 +191,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
|
|||||||
|
|
||||||
BdfList signed =
|
BdfList signed =
|
||||||
BdfList.of(blog.getId(), message.getTimestamp(), body);
|
BdfList.of(blog.getId(), message.getTimestamp(), body);
|
||||||
expectCrypto(signed, sigBytes, true);
|
expectCrypto(signed, sigBytes);
|
||||||
|
|
||||||
final BdfList originalList = BdfList.of(POST.getInt(), body, sigBytes);
|
final BdfList originalList = BdfList.of(POST.getInt(), body, sigBytes);
|
||||||
final byte[] originalBody = TestUtils.getRandomBytes(42);
|
final byte[] originalBody = TestUtils.getRandomBytes(42);
|
||||||
@@ -247,7 +230,7 @@ public class BlogPostValidatorTest extends BriarTestCase {
|
|||||||
|
|
||||||
BdfList signed = BdfList.of(blog.getId(), message.getTimestamp(),
|
BdfList signed = BdfList.of(blog.getId(), message.getTimestamp(),
|
||||||
comment, originalId, oldId);
|
comment, originalId, oldId);
|
||||||
expectCrypto(signed, sigBytes, true);
|
expectCrypto(signed, sigBytes);
|
||||||
|
|
||||||
final BdfList originalList = BdfList.of(COMMENT.getInt(), comment,
|
final BdfList originalList = BdfList.of(COMMENT.getInt(), comment,
|
||||||
originalId, oldId, sigBytes);
|
originalId, oldId, sigBytes);
|
||||||
@@ -275,27 +258,13 @@ public class BlogPostValidatorTest extends BriarTestCase {
|
|||||||
context.assertIsSatisfied();
|
context.assertIsSatisfied();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void expectCrypto(final BdfList signed, final byte[] sig,
|
private void expectCrypto(final BdfList signed, final byte[] sig)
|
||||||
final boolean pass) throws IOException, GeneralSecurityException {
|
throws IOException, GeneralSecurityException {
|
||||||
final Signature signature = context.mock(Signature.class);
|
|
||||||
final KeyParser keyParser = context.mock(KeyParser.class);
|
|
||||||
final PublicKey publicKey = context.mock(PublicKey.class);
|
|
||||||
|
|
||||||
context.checking(new Expectations() {{
|
context.checking(new Expectations() {{
|
||||||
oneOf(blogFactory).parseBlog(group, "");
|
oneOf(blogFactory).parseBlog(group, "");
|
||||||
will(returnValue(blog));
|
will(returnValue(blog));
|
||||||
oneOf(cryptoComponent).getSignatureKeyParser();
|
oneOf(clientHelper)
|
||||||
will(returnValue(keyParser));
|
.verifySignature(sig, author.getPublicKey(), signed);
|
||||||
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));
|
|
||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user