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:
akwizgran
2016-10-31 15:17:51 +00:00
48 changed files with 1866 additions and 459 deletions

View File

@@ -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;

View File

@@ -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());
}
}

View File

@@ -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();
}

View File

@@ -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"

View 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>

View File

@@ -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>

View File

@@ -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;
} }

View File

@@ -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();

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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);
} }

View File

@@ -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);
} }
} }

View File

@@ -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;
}
} }

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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);

View File

@@ -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();
} }

View File

@@ -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;
} }
} }

View File

@@ -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);

View File

@@ -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("");

View File

@@ -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);

View File

@@ -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

View File

@@ -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);
}
});
}
}

View File

@@ -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());

View File

@@ -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;
} }

View File

@@ -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;

View File

@@ -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;
} }

View File

@@ -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;
} }
} }

View File

@@ -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);
} }

View File

@@ -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;
} }

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
} }

View File

@@ -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()),

View File

@@ -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;

View File

@@ -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");
}
}
} }

View File

@@ -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;

View File

@@ -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

View File

@@ -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";
} }

View File

@@ -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);
}
} }
} }

View File

@@ -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);
}
} }

View File

@@ -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");
}
} }
} }

View File

@@ -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;
} }

View File

@@ -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));
}}); }});
} }