diff --git a/briar-api/build.gradle b/briar-api/build.gradle index d38a8b52035036596caac62784a2fbe3da2bd767..479b38fdcba2e4c60c74408cf16460b445c8da50 100644 --- a/briar-api/build.gradle +++ b/briar-api/build.gradle @@ -7,14 +7,16 @@ apply plugin: 'witness' dependencies { compile "com.google.dagger:dagger:2.0.2" compile 'com.google.dagger:dagger-compiler:2.0.2' + compile 'org.jetbrains:annotations-java5:15.0' } dependencyVerification { verify = [ 'com.google.dagger:dagger:84c0282ed8be73a29e0475d639da030b55dee72369e58dd35ae7d4fe6243dcf9', 'com.google.dagger:dagger-compiler:b74bc9de063dd4c6400b232231f2ef5056145b8fbecbf5382012007dd1c071b3', - 'com.google.dagger:dagger-producers:99ec15e8a0507ba569e7655bc1165ee5e5ca5aa914b3c8f7e2c2458f724edd6b', + 'org.jetbrains:annotations-java5:c84e6e9947f802ec2183bdc415dd496df02a749cac92e805f697e60f628a1e24', 'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff', + 'com.google.dagger:dagger-producers:99ec15e8a0507ba569e7655bc1165ee5e5ca5aa914b3c8f7e2c2458f724edd6b', 'com.google.guava:guava:d664fbfc03d2e5ce9cab2a44fb01f1d0bf9dfebeccc1a473b1f9ea31f79f6f99', ] } diff --git a/briar-api/src/org/briarproject/api/blogs/Blog.java b/briar-api/src/org/briarproject/api/blogs/Blog.java new file mode 100644 index 0000000000000000000000000000000000000000..c97e42be2aae72185845fa2b5ea05ee257ebd17c --- /dev/null +++ b/briar-api/src/org/briarproject/api/blogs/Blog.java @@ -0,0 +1,32 @@ +package org.briarproject.api.blogs; + +import org.briarproject.api.forum.Forum; +import org.briarproject.api.identity.Author; +import org.briarproject.api.sync.Group; +import org.jetbrains.annotations.NotNull; + +public class Blog extends Forum { + + @NotNull + private final String description; + @NotNull + private final Author author; + + public Blog(@NotNull Group group, @NotNull String name, + @NotNull String description, @NotNull Author author) { + super(group, name, null); + + this.description = description; + this.author = author; + } + + @NotNull + public String getDescription() { + return description; + } + + @NotNull + public Author getAuthor() { + return author; + } +} diff --git a/briar-api/src/org/briarproject/api/blogs/BlogConstants.java b/briar-api/src/org/briarproject/api/blogs/BlogConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..60f06a641b5591ed79c35d32e785d3265358cff6 --- /dev/null +++ b/briar-api/src/org/briarproject/api/blogs/BlogConstants.java @@ -0,0 +1,39 @@ +package org.briarproject.api.blogs; + +import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH; + +public interface BlogConstants { + + /** The maximum length of a blogs's name in UTF-8 bytes. */ + int MAX_BLOG_TITLE_LENGTH = 100; + + /** The length of a blogs's description in UTF-8 bytes. */ + int MAX_BLOG_DESC_LENGTH = 240; + + /** The maximum length of a blog post's content type in UTF-8 bytes. */ + int MAX_CONTENT_TYPE_LENGTH = 50; + + /** The length of a blog post's title in UTF-8 bytes. */ + int MAX_BLOG_POST_TITLE_LENGTH = 100; + + /** The length of a blog post's teaser in UTF-8 bytes. */ + int MAX_BLOG_POST_TEASER_LENGTH = 240; + + /** The maximum length of a blog post's body in bytes. */ + int MAX_BLOG_POST_BODY_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024; + + // Metadata keys + String KEY_DESCRIPTION = "description"; + String KEY_TITLE = "title"; + String KEY_TEASER = "teaser"; + String KEY_HAS_BODY = "hasBody"; + String KEY_TIMESTAMP = "timestamp"; + String KEY_PARENT = "parent"; + String KEY_AUTHOR_ID = "id"; + String KEY_AUTHOR_NAME = "name"; + String KEY_PUBLIC_KEY = "publicKey"; + String KEY_AUTHOR = "author"; + String KEY_CONTENT_TYPE = "contentType"; + String KEY_READ = "read"; + +} diff --git a/briar-api/src/org/briarproject/api/blogs/BlogFactory.java b/briar-api/src/org/briarproject/api/blogs/BlogFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..8da9db1926f11d4dc31ed633fb61d794aa1444fb --- /dev/null +++ b/briar-api/src/org/briarproject/api/blogs/BlogFactory.java @@ -0,0 +1,17 @@ +package org.briarproject.api.blogs; + +import org.briarproject.api.FormatException; +import org.briarproject.api.identity.Author; +import org.briarproject.api.sync.Group; +import org.jetbrains.annotations.NotNull; + +public interface BlogFactory { + + /** Creates a blog with the given name, description and author. */ + Blog createBlog(@NotNull String name, @NotNull String description, + @NotNull Author author); + + /** Parses a blog with the given Group and description */ + Blog parseBlog(@NotNull Group g, @NotNull String description) + throws FormatException; +} diff --git a/briar-api/src/org/briarproject/api/blogs/BlogManager.java b/briar-api/src/org/briarproject/api/blogs/BlogManager.java new file mode 100644 index 0000000000000000000000000000000000000000..e01fa5efb9a8bc30cf6dbd4dd3f97d063dae713c --- /dev/null +++ b/briar-api/src/org/briarproject/api/blogs/BlogManager.java @@ -0,0 +1,48 @@ +package org.briarproject.api.blogs; + +import org.briarproject.api.FormatException; +import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Transaction; +import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.sync.ClientId; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.MessageId; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; + +public interface BlogManager { + + /** Returns the unique ID of the blog client. */ + ClientId getClientId(); + + /** Creates a new Blog. */ + Blog addBlog(LocalAuthor localAuthor, String name, String description) + throws DbException; + + /** Stores a local blog post. */ + void addLocalPost(BlogPost p) throws DbException; + + /** Returns the blog with the given ID. */ + Blog getBlog(GroupId g) throws DbException; + + /** Returns the blog with the given ID. */ + Blog getBlog(Transaction txn, GroupId g) throws DbException; + + /** Returns all blogs to which the localAuthor created. */ + Collection<Blog> getBlogs(LocalAuthor localAuthor) throws DbException; + + /** Returns all blogs to which the user subscribes. */ + Collection<Blog> getBlogs() throws DbException; + + /** Returns the body of the blog post with the given ID. */ + @Nullable + byte[] getPostBody(MessageId m) throws DbException; + + /** Returns the headers of all posts in the given blog. */ + Collection<BlogPostHeader> getPostHeaders(GroupId g) throws DbException; + + /** Marks a blog post as read or unread. */ + void setReadFlag(MessageId m, boolean read) throws DbException; + +} diff --git a/briar-api/src/org/briarproject/api/blogs/BlogPost.java b/briar-api/src/org/briarproject/api/blogs/BlogPost.java new file mode 100644 index 0000000000000000000000000000000000000000..a6aa1167aebc9c24ac00bfc81fa1f5ffc157a064 --- /dev/null +++ b/briar-api/src/org/briarproject/api/blogs/BlogPost.java @@ -0,0 +1,43 @@ +package org.briarproject.api.blogs; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import org.briarproject.api.forum.ForumPost; +import org.briarproject.api.identity.Author; +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageId; + +public class BlogPost extends ForumPost { + + @Nullable + private final String title; + @NotNull + private final String teaser; + private final boolean hasBody; + + public BlogPost(@Nullable String title, @NotNull String teaser, + boolean hasBody, @NotNull Message message, + @Nullable MessageId parent, @NotNull Author author, + @NotNull String contentType) { + super(message, parent, author, contentType); + + this.title = title; + this.teaser = teaser; + this.hasBody = hasBody; + } + + @Nullable + public String getTitle() { + return title; + } + + @NotNull + public String getTeaser() { + return teaser; + } + + public boolean hasBody() { + return hasBody; + } +} diff --git a/briar-api/src/org/briarproject/api/blogs/BlogPostFactory.java b/briar-api/src/org/briarproject/api/blogs/BlogPostFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..2565bfb0e731dc532e8cdc0a691538f8c8166d3a --- /dev/null +++ b/briar-api/src/org/briarproject/api/blogs/BlogPostFactory.java @@ -0,0 +1,19 @@ +package org.briarproject.api.blogs; + +import org.briarproject.api.FormatException; +import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.MessageId; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.security.GeneralSecurityException; + +public interface BlogPostFactory { + + BlogPost createBlogPost(@NotNull GroupId groupId, @Nullable String title, + @NotNull String teaser, long timestamp, @Nullable MessageId parent, + @NotNull LocalAuthor author, @NotNull String contentType, + @Nullable byte[] body) + throws FormatException, GeneralSecurityException; +} diff --git a/briar-api/src/org/briarproject/api/blogs/BlogPostHeader.java b/briar-api/src/org/briarproject/api/blogs/BlogPostHeader.java new file mode 100644 index 0000000000000000000000000000000000000000..24d421c13959ddb173c91130f70f42bad408bc07 --- /dev/null +++ b/briar-api/src/org/briarproject/api/blogs/BlogPostHeader.java @@ -0,0 +1,43 @@ +package org.briarproject.api.blogs; + +import org.briarproject.api.clients.PostHeader; +import org.briarproject.api.identity.Author; +import org.briarproject.api.identity.Author.Status; +import org.briarproject.api.sync.MessageId; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class BlogPostHeader extends PostHeader { + + @Nullable + private final String title; + @NotNull + private final String teaser; + private final boolean hasBody; + + public BlogPostHeader(@Nullable String title, @NotNull String teaser, + boolean hasBody, @NotNull MessageId id, + @Nullable MessageId parentId, long timestamp, + @NotNull Author author, @NotNull Status authorStatus, + @NotNull String contentType, boolean read) { + super(id, parentId, timestamp, author, authorStatus, contentType, read); + + this.title = title; + this.teaser = teaser; + this.hasBody = hasBody; + } + + @Nullable + public String getTitle() { + return title; + } + + @NotNull + public String getTeaser() { + return teaser; + } + + public boolean hasBody() { + return hasBody; + } +} diff --git a/briar-api/src/org/briarproject/api/clients/PostHeader.java b/briar-api/src/org/briarproject/api/clients/PostHeader.java new file mode 100644 index 0000000000000000000000000000000000000000..33f0d28771c9ecc7f30eeb87798d18b3b3f4c1b2 --- /dev/null +++ b/briar-api/src/org/briarproject/api/clients/PostHeader.java @@ -0,0 +1,55 @@ +package org.briarproject.api.clients; + +import org.briarproject.api.identity.Author; +import org.briarproject.api.sync.MessageId; + +public abstract class PostHeader { + + private final MessageId id; + private final MessageId parentId; + private final long timestamp; + private final Author author; + private final Author.Status authorStatus; + private final String contentType; + private final boolean read; + + public PostHeader(MessageId id, MessageId parentId, long timestamp, + Author author, Author.Status authorStatus, String contentType, + boolean read) { + this.id = id; + this.parentId = parentId; + this.timestamp = timestamp; + this.author = author; + this.authorStatus = authorStatus; + this.contentType = contentType; + this.read = read; + } + + public MessageId getId() { + return id; + } + + public Author getAuthor() { + return author; + } + + public Author.Status getAuthorStatus() { + return authorStatus; + } + + public String getContentType() { + return contentType; + } + + public long getTimestamp() { + return timestamp; + } + + public boolean isRead() { + return read; + } + + public MessageId getParentId() { + return parentId; + } +} diff --git a/briar-api/src/org/briarproject/api/forum/ForumPostHeader.java b/briar-api/src/org/briarproject/api/forum/ForumPostHeader.java index 40779390539cd3dff457cc70470d1443780f1755..c25a9e251b1436e3beda8207656abba1112a0674 100644 --- a/briar-api/src/org/briarproject/api/forum/ForumPostHeader.java +++ b/briar-api/src/org/briarproject/api/forum/ForumPostHeader.java @@ -1,56 +1,17 @@ package org.briarproject.api.forum; import org.briarproject.api.clients.MessageTree; +import org.briarproject.api.clients.PostHeader; import org.briarproject.api.identity.Author; import org.briarproject.api.sync.MessageId; -public class ForumPostHeader implements MessageTree.MessageNode { - - private final MessageId id; - private final MessageId parentId; - private final long timestamp; - private final Author author; - private final Author.Status authorStatus; - private final String contentType; - private final boolean read; +public class ForumPostHeader extends PostHeader + implements MessageTree.MessageNode { public ForumPostHeader(MessageId id, MessageId parentId, long timestamp, Author author, Author.Status authorStatus, String contentType, boolean read) { - this.id = id; - this.parentId = parentId; - this.timestamp = timestamp; - this.author = author; - this.authorStatus = authorStatus; - this.contentType = contentType; - this.read = read; - } - - public MessageId getId() { - return id; - } - - public Author getAuthor() { - return author; - } - - public Author.Status getAuthorStatus() { - return authorStatus; + super(id, parentId, timestamp, author, authorStatus, contentType, read); } - public String getContentType() { - return contentType; - } - - public long getTimestamp() { - return timestamp; - } - - public boolean isRead() { - return read; - } - - public MessageId getParentId() { - return parentId; - } } diff --git a/briar-api/src/org/briarproject/api/identity/IdentityManager.java b/briar-api/src/org/briarproject/api/identity/IdentityManager.java index f2a4db2d906f1a6cec2e8292ce7a7437d94ae158..e5420a043f267c9a643fd24987369552839e560e 100644 --- a/briar-api/src/org/briarproject/api/identity/IdentityManager.java +++ b/briar-api/src/org/briarproject/api/identity/IdentityManager.java @@ -2,6 +2,7 @@ package org.briarproject.api.identity; import org.briarproject.api.db.DbException; import org.briarproject.api.db.Transaction; +import org.briarproject.api.identity.Author.Status; import java.util.Collection; @@ -25,6 +26,9 @@ public interface IdentityManager { /** Removes a local pseudonym and all associated state. */ void removeLocalAuthor(AuthorId a) throws DbException; + /** Returns the trust-level status of the author */ + Status getAuthorStatus(AuthorId a) throws DbException; + interface AddIdentityHook { void addingIdentity(Transaction txn, LocalAuthor a) throws DbException; } diff --git a/briar-core/src/org/briarproject/CoreEagerSingletons.java b/briar-core/src/org/briarproject/CoreEagerSingletons.java index 174ee9c570b572e77861cff17229a19f0f9d8a71..6847f461220bebd4b3131b09af88e6d253c5c677 100644 --- a/briar-core/src/org/briarproject/CoreEagerSingletons.java +++ b/briar-core/src/org/briarproject/CoreEagerSingletons.java @@ -1,5 +1,6 @@ package org.briarproject; +import org.briarproject.blogs.BlogsModule; import org.briarproject.contact.ContactModule; import org.briarproject.crypto.CryptoModule; import org.briarproject.db.DatabaseExecutorModule; @@ -15,6 +16,8 @@ import org.briarproject.transport.TransportModule; public interface CoreEagerSingletons { + void inject(BlogsModule.EagerSingletons init); + void inject(ContactModule.EagerSingletons init); void inject(CryptoModule.EagerSingletons init); diff --git a/briar-core/src/org/briarproject/CoreModule.java b/briar-core/src/org/briarproject/CoreModule.java index 238c859e7fa8f1d79b00613c14e92938d7ace480..6a909b3f6d8110b63a4348cee50012283ef6d2d2 100644 --- a/briar-core/src/org/briarproject/CoreModule.java +++ b/briar-core/src/org/briarproject/CoreModule.java @@ -1,5 +1,6 @@ package org.briarproject; +import org.briarproject.blogs.BlogsModule; import org.briarproject.clients.ClientsModule; import org.briarproject.contact.ContactModule; import org.briarproject.crypto.CryptoModule; @@ -26,6 +27,7 @@ import org.briarproject.transport.TransportModule; import dagger.Module; @Module(includes = { + BlogsModule.class, ClientsModule.class, ContactModule.class, CryptoModule.class, @@ -52,6 +54,7 @@ import dagger.Module; public class CoreModule { public static void initEagerSingletons(CoreEagerSingletons c) { + c.inject(new BlogsModule.EagerSingletons()); c.inject(new ContactModule.EagerSingletons()); c.inject(new CryptoModule.EagerSingletons()); c.inject(new DatabaseExecutorModule.EagerSingletons()); diff --git a/briar-core/src/org/briarproject/blogs/BlogFactoryImpl.java b/briar-core/src/org/briarproject/blogs/BlogFactoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..b04401f5d1297854af0e45c6694afa5f26edc796 --- /dev/null +++ b/briar-core/src/org/briarproject/blogs/BlogFactoryImpl.java @@ -0,0 +1,61 @@ +package org.briarproject.blogs; + +import org.briarproject.api.FormatException; +import org.briarproject.api.blogs.Blog; +import org.briarproject.api.blogs.BlogFactory; +import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.data.BdfList; +import org.briarproject.api.identity.Author; +import org.briarproject.api.identity.AuthorFactory; +import org.briarproject.api.sync.Group; +import org.briarproject.api.sync.GroupFactory; +import org.jetbrains.annotations.NotNull; + +import javax.inject.Inject; + +class BlogFactoryImpl implements BlogFactory { + + private final GroupFactory groupFactory; + private final AuthorFactory authorFactory; + private final ClientHelper clientHelper; + + @Inject + BlogFactoryImpl(GroupFactory groupFactory, AuthorFactory authorFactory, + ClientHelper clientHelper) { + + this.groupFactory = groupFactory; + this.authorFactory = authorFactory; + this.clientHelper = clientHelper; + } + + @Override + public Blog createBlog(@NotNull String name, @NotNull String description, + @NotNull Author author) { + try { + BdfList blog = BdfList.of( + name, + author.getName(), + author.getPublicKey() + ); + byte[] descriptor = clientHelper.toByteArray(blog); + Group g = groupFactory + .createGroup(BlogManagerImpl.CLIENT_ID, descriptor); + return new Blog(g, name, description, author); + } catch (FormatException e) { + throw new RuntimeException(e); + } + } + + @Override + public Blog parseBlog(@NotNull Group g, @NotNull String description) + throws FormatException { + + byte[] descriptor = g.getDescriptor(); + // Blog Name, Author Name, Public Key + BdfList blog = clientHelper.toList(descriptor, 0, descriptor.length); + Author a = + authorFactory.createAuthor(blog.getString(1), blog.getRaw(2)); + return new Blog(g, blog.getString(0), description, a); + } + +} diff --git a/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java b/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..eabfc251e3c7284557952ca8766196646cb063a9 --- /dev/null +++ b/briar-core/src/org/briarproject/blogs/BlogManagerImpl.java @@ -0,0 +1,260 @@ +package org.briarproject.blogs; + +import org.briarproject.api.FormatException; +import org.briarproject.api.blogs.Blog; +import org.briarproject.api.blogs.BlogFactory; +import org.briarproject.api.blogs.BlogManager; +import org.briarproject.api.blogs.BlogPost; +import org.briarproject.api.blogs.BlogPostHeader; +import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfEntry; +import org.briarproject.api.data.BdfList; +import org.briarproject.api.db.DatabaseComponent; +import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Transaction; +import org.briarproject.api.identity.Author; +import org.briarproject.api.identity.Author.Status; +import org.briarproject.api.identity.AuthorId; +import org.briarproject.api.identity.IdentityManager; +import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.sync.ClientId; +import org.briarproject.api.sync.Group; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.MessageId; +import org.briarproject.util.StringUtils; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR; +import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR_ID; +import static org.briarproject.api.blogs.BlogConstants.KEY_AUTHOR_NAME; +import static org.briarproject.api.blogs.BlogConstants.KEY_CONTENT_TYPE; +import static org.briarproject.api.blogs.BlogConstants.KEY_DESCRIPTION; +import static org.briarproject.api.blogs.BlogConstants.KEY_HAS_BODY; +import static org.briarproject.api.blogs.BlogConstants.KEY_PARENT; +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_TEASER; +import static org.briarproject.api.blogs.BlogConstants.KEY_TIMESTAMP; +import static org.briarproject.api.blogs.BlogConstants.KEY_TITLE; + +class BlogManagerImpl implements BlogManager { + + private static final Logger LOG = + Logger.getLogger(BlogManagerImpl.class.getName()); + + static final ClientId CLIENT_ID = new ClientId(StringUtils.fromHexString( + "dafbe56f0c8971365cea4bb5f08ec9a6" + + "1d686e058b943997b6ff259ba423f613")); + + private final DatabaseComponent db; + private final IdentityManager identityManager; + private final ClientHelper clientHelper; + private final BlogFactory blogFactory; + + @Inject + BlogManagerImpl(DatabaseComponent db, IdentityManager identityManager, + ClientHelper clientHelper, BlogFactory blogFactory) { + + this.db = db; + this.identityManager = identityManager; + this.clientHelper = clientHelper; + this.blogFactory = blogFactory; + } + + @Override + public ClientId getClientId() { + return CLIENT_ID; + } + + @Override + public Blog addBlog(LocalAuthor localAuthor, String name, + String description) throws DbException { + + Blog b = blogFactory + .createBlog(name, description, localAuthor); + BdfDictionary metadata = BdfDictionary.of( + new BdfEntry(KEY_DESCRIPTION, b.getDescription()) + ); + + Transaction txn = db.startTransaction(false); + try { + db.addGroup(txn, b.getGroup()); + clientHelper.mergeGroupMetadata(txn, b.getId(), metadata); + txn.setComplete(); + } catch (FormatException e) { + throw new DbException(e); + } finally { + db.endTransaction(txn); + } + return b; + } + + @Override + public void addLocalPost(BlogPost p) throws DbException { + try { + BdfDictionary meta = new BdfDictionary(); + if (p.getTitle() != null) meta.put(KEY_TITLE, p.getTitle()); + meta.put(KEY_TEASER, p.getTeaser()); + meta.put(KEY_TIMESTAMP, p.getMessage().getTimestamp()); + meta.put(KEY_HAS_BODY, p.hasBody()); + if (p.getParent() != null) meta.put(KEY_PARENT, p.getParent()); + + Author a = p.getAuthor(); + BdfDictionary authorMeta = new BdfDictionary(); + authorMeta.put(KEY_AUTHOR_ID, a.getId()); + authorMeta.put(KEY_AUTHOR_NAME, a.getName()); + authorMeta.put(KEY_PUBLIC_KEY, a.getPublicKey()); + meta.put(KEY_AUTHOR, authorMeta); + + meta.put(KEY_CONTENT_TYPE, p.getContentType()); + meta.put(KEY_READ, true); + clientHelper.addLocalMessage(p.getMessage(), CLIENT_ID, meta, true); + } catch (FormatException e) { + throw new RuntimeException(e); + } + } + + @Override + public Blog getBlog(GroupId g) throws DbException { + Blog blog; + Transaction txn = db.startTransaction(true); + try { + blog = getBlog(txn, g); + txn.setComplete(); + } finally { + db.endTransaction(txn); + } + return blog; + } + + @Override + public Blog getBlog(Transaction txn, GroupId g) throws DbException { + try { + Group group = db.getGroup(txn, g); + String description = getBlogDescription(txn, g); + return blogFactory.parseBlog(group, description); + } catch (FormatException e) { + throw new DbException(e); + } + } + + @Override + public Collection<Blog> getBlogs(LocalAuthor localAuthor) + throws DbException { + + Collection<Blog> allBlogs = getBlogs(); + List<Blog> blogs = new ArrayList<Blog>(); + for (Blog b : allBlogs) { + if (b.getAuthor().equals(localAuthor)) { + blogs.add(b); + } + } + return Collections.unmodifiableList(blogs); + } + + @Override + public Collection<Blog> getBlogs() throws DbException { + try { + List<Blog> blogs = new ArrayList<Blog>(); + Collection<Group> groups; + Transaction txn = db.startTransaction(true); + try { + groups = db.getGroups(txn, CLIENT_ID); + for (Group g : groups) { + String description = getBlogDescription(txn, g.getId()); + blogs.add(blogFactory.parseBlog(g, description)); + } + txn.setComplete(); + } finally { + db.endTransaction(txn); + } + return Collections.unmodifiableList(blogs); + } catch (FormatException e) { + throw new DbException(e); + } + } + + @Override + @Nullable + public byte[] getPostBody(MessageId m) throws DbException { + try { + // content, signature + // content: parent, contentType, title, teaser, body, attachments + BdfList message = clientHelper.getMessageAsList(m); + BdfList content = message.getList(0); + return content.getRaw(4); + } catch (FormatException e) { + throw new DbException(e); + } + } + + @Override + public Collection<BlogPostHeader> getPostHeaders(GroupId g) + throws DbException { + + Map<MessageId, BdfDictionary> metadata; + try { + metadata = clientHelper.getMessageMetadataAsDictionary(g); + } catch (FormatException e) { + throw new DbException(e); + } + Collection<BlogPostHeader> headers = new ArrayList<BlogPostHeader>(); + for (Entry<MessageId, BdfDictionary> entry : metadata.entrySet()) { + try { + BdfDictionary meta = entry.getValue(); + String title = meta.getOptionalString(KEY_TITLE); + String teaser = meta.getString(KEY_TEASER); + boolean hasBody = meta.getBoolean(KEY_HAS_BODY); + long timestamp = meta.getLong(KEY_TIMESTAMP); + MessageId parentId = null; + if (meta.containsKey(KEY_PARENT)) + parentId = new MessageId(meta.getRaw(KEY_PARENT)); + + BdfDictionary d = meta.getDictionary(KEY_AUTHOR); + AuthorId authorId = new AuthorId(d.getRaw(KEY_AUTHOR_ID)); + String name = d.getString(KEY_AUTHOR_NAME); + byte[] publicKey = d.getRaw(KEY_PUBLIC_KEY); + Author author = new Author(authorId, name, publicKey); + Status authorStatus = identityManager.getAuthorStatus(authorId); + + String contentType = meta.getString(KEY_CONTENT_TYPE); + boolean read = meta.getBoolean(KEY_READ); + headers.add(new BlogPostHeader(title, teaser, hasBody, + entry.getKey(), parentId, timestamp, author, + authorStatus, contentType, read)); + } catch (FormatException e) { + throw new DbException(e); + } + } + return headers; + } + + @Override + public void setReadFlag(MessageId m, boolean read) throws DbException { + try { + BdfDictionary meta = new BdfDictionary(); + meta.put(KEY_READ, read); + clientHelper.mergeMessageMetadata(m, meta); + } catch (FormatException e) { + throw new RuntimeException(e); + } + } + + private String getBlogDescription(Transaction txn, GroupId g) + throws DbException, FormatException { + BdfDictionary d = clientHelper.getGroupMetadataAsDictionary(txn, g); + return d.getString(KEY_DESCRIPTION); + } + +} diff --git a/briar-core/src/org/briarproject/blogs/BlogPostFactoryImpl.java b/briar-core/src/org/briarproject/blogs/BlogPostFactoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..e9ced2d3fe86ec555eb19e580e4c8ec5a83cdbcc --- /dev/null +++ b/briar-core/src/org/briarproject/blogs/BlogPostFactoryImpl.java @@ -0,0 +1,78 @@ +package org.briarproject.blogs; + +import org.briarproject.api.FormatException; +import org.briarproject.api.blogs.BlogPost; +import org.briarproject.api.blogs.BlogPostFactory; +import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.crypto.KeyParser; +import org.briarproject.api.crypto.PrivateKey; +import org.briarproject.api.crypto.Signature; +import org.briarproject.api.data.BdfList; +import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageId; +import org.briarproject.util.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.security.GeneralSecurityException; + +import javax.inject.Inject; + +import static org.briarproject.api.blogs.BlogConstants.MAX_BLOG_POST_BODY_LENGTH; +import static org.briarproject.api.blogs.BlogConstants.MAX_BLOG_POST_TEASER_LENGTH; +import static org.briarproject.api.blogs.BlogConstants.MAX_BLOG_POST_TITLE_LENGTH; +import static org.briarproject.api.blogs.BlogConstants.MAX_CONTENT_TYPE_LENGTH; + +class BlogPostFactoryImpl implements BlogPostFactory { + + private final CryptoComponent crypto; + private final ClientHelper clientHelper; + + @Inject + BlogPostFactoryImpl(CryptoComponent crypto, ClientHelper clientHelper) { + this.crypto = crypto; + this.clientHelper = clientHelper; + } + + @Override + public BlogPost createBlogPost(@NotNull GroupId groupId, + @Nullable String title, @NotNull String teaser, long timestamp, + @Nullable MessageId parent, @NotNull LocalAuthor author, + @NotNull String contentType, @Nullable byte[] body) + throws FormatException, GeneralSecurityException { + + // Validate the arguments + if (title != null && + StringUtils.toUtf8(title).length > MAX_BLOG_POST_TITLE_LENGTH) + throw new IllegalArgumentException(); + if (StringUtils.toUtf8(teaser).length > MAX_BLOG_POST_TEASER_LENGTH) + throw new IllegalArgumentException(); + if (StringUtils.toUtf8(contentType).length > MAX_CONTENT_TYPE_LENGTH) + throw new IllegalArgumentException(); + if (body != null && body.length > MAX_BLOG_POST_BODY_LENGTH) + throw new IllegalArgumentException(); + + // Serialise the data to be signed + BdfList content = + BdfList.of(parent, contentType, title, teaser, body, null); + BdfList signed = BdfList.of(groupId, timestamp, content); + + // Generate the signature + Signature signature = crypto.getSignature(); + KeyParser keyParser = crypto.getSignatureKeyParser(); + PrivateKey privateKey = + keyParser.parsePrivateKey(author.getPrivateKey()); + signature.initSign(privateKey); + signature.update(clientHelper.toByteArray(signed)); + byte[] sig = signature.sign(); + + // Serialise the signed message + BdfList message = BdfList.of(content, sig); + Message m = clientHelper.createMessage(groupId, timestamp, message); + return new BlogPost(title, teaser, body != null, m, parent, author, + contentType); + } +} diff --git a/briar-core/src/org/briarproject/blogs/BlogPostValidator.java b/briar-core/src/org/briarproject/blogs/BlogPostValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..bacf02dff64d45e59ca80113158cba6e2778c206 --- /dev/null +++ b/briar-core/src/org/briarproject/blogs/BlogPostValidator.java @@ -0,0 +1,125 @@ +package org.briarproject.blogs; + +import org.briarproject.api.FormatException; +import org.briarproject.api.UniqueId; +import org.briarproject.api.blogs.Blog; +import org.briarproject.api.blogs.BlogFactory; +import org.briarproject.api.clients.BdfMessageContext; +import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.crypto.KeyParser; +import org.briarproject.api.crypto.PublicKey; +import org.briarproject.api.crypto.Signature; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfList; +import org.briarproject.api.data.MetadataEncoder; +import org.briarproject.api.identity.Author; +import org.briarproject.api.sync.Group; +import org.briarproject.api.sync.InvalidMessageException; +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageId; +import org.briarproject.api.system.Clock; +import org.briarproject.clients.BdfMessageValidator; + +import java.security.GeneralSecurityException; +import java.util.Collection; +import java.util.Collections; + +import static org.briarproject.api.blogs.BlogConstants.KEY_CONTENT_TYPE; +import static org.briarproject.api.blogs.BlogConstants.KEY_HAS_BODY; +import static org.briarproject.api.blogs.BlogConstants.KEY_PARENT; +import static org.briarproject.api.blogs.BlogConstants.KEY_READ; +import static org.briarproject.api.blogs.BlogConstants.KEY_TEASER; +import static org.briarproject.api.blogs.BlogConstants.KEY_TIMESTAMP; +import static org.briarproject.api.blogs.BlogConstants.KEY_TITLE; +import static org.briarproject.api.blogs.BlogConstants.MAX_BLOG_POST_BODY_LENGTH; +import static org.briarproject.api.blogs.BlogConstants.MAX_BLOG_POST_TEASER_LENGTH; +import static org.briarproject.api.blogs.BlogConstants.MAX_BLOG_POST_TITLE_LENGTH; +import static org.briarproject.api.blogs.BlogConstants.MAX_CONTENT_TYPE_LENGTH; +import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH; + +class BlogPostValidator extends BdfMessageValidator { + + private final CryptoComponent crypto; + private final BlogFactory blogFactory; + + BlogPostValidator(CryptoComponent crypto, BlogFactory blogFactory, + ClientHelper clientHelper, MetadataEncoder metadataEncoder, + Clock clock) { + super(clientHelper, metadataEncoder, clock); + + this.crypto = crypto; + this.blogFactory = blogFactory; + } + + @Override + protected BdfMessageContext validateMessage(Message m, Group g, + BdfList body) throws InvalidMessageException, FormatException { + + // Content, Signature + checkSize(body, 2); + BdfList content = body.getList(0); + + // Content: Parent ID, content type, title (optional), teaser, + // post body (optional), attachments (optional) + checkSize(body, 6); + // Parent ID is optional + byte[] parent = content.getOptionalRaw(0); + checkLength(parent, UniqueId.LENGTH); + // Content type + String contentType = content.getString(1); + checkLength(contentType, 0, MAX_CONTENT_TYPE_LENGTH); + // Blog post title is optional + String title = content.getOptionalString(2); + checkLength(contentType, 0, MAX_BLOG_POST_TITLE_LENGTH); + // Blog teaser + String teaser = content.getString(3); + // TODO make sure that there is only text in the teaser + checkLength(contentType, 0, MAX_BLOG_POST_TEASER_LENGTH); + // Blog post body is optional + byte[] postBody = content.getOptionalRaw(4); + checkLength(postBody, 0, MAX_BLOG_POST_BODY_LENGTH); + // Attachments + BdfDictionary attachments = content.getOptionalDictionary(5); + // TODO handle attachments somehow + + // Signature + byte[] sig = body.getRaw(1); + checkLength(sig, 0, MAX_SIGNATURE_LENGTH); + // Verify the signature + try { + // Get the blog author + Blog b = blogFactory.parseBlog(g, ""); // description doesn't matter + Author a = b.getAuthor(); + // Parse the public key + KeyParser keyParser = crypto.getSignatureKeyParser(); + PublicKey key = keyParser.parsePublicKey(a.getPublicKey()); + // Serialise the data to be signed + BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), content); + // 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"); + } + + // Return the metadata and dependencies + BdfDictionary meta = new BdfDictionary(); + Collection<MessageId> dependencies = null; + if (title != null) meta.put(KEY_TITLE, title); + meta.put(KEY_TEASER, teaser); + meta.put(KEY_HAS_BODY, postBody != null); + meta.put(KEY_TIMESTAMP, m.getTimestamp()); + if (parent != null) { + meta.put(KEY_PARENT, parent); + dependencies = Collections.singletonList(new MessageId(parent)); + } + meta.put(KEY_CONTENT_TYPE, contentType); + meta.put(KEY_READ, false); + return new BdfMessageContext(meta, dependencies); + } +} diff --git a/briar-core/src/org/briarproject/blogs/BlogsModule.java b/briar-core/src/org/briarproject/blogs/BlogsModule.java new file mode 100644 index 0000000000000000000000000000000000000000..4cf7894d1b608b2557a8098f5509a1ebb6ec7b18 --- /dev/null +++ b/briar-core/src/org/briarproject/blogs/BlogsModule.java @@ -0,0 +1,63 @@ +package org.briarproject.blogs; + +import org.briarproject.api.blogs.BlogFactory; +import org.briarproject.api.blogs.BlogManager; +import org.briarproject.api.blogs.BlogPostFactory; +import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.data.MetadataEncoder; +import org.briarproject.api.db.DatabaseComponent; +import org.briarproject.api.identity.AuthorFactory; +import org.briarproject.api.identity.IdentityManager; +import org.briarproject.api.sync.GroupFactory; +import org.briarproject.api.sync.ValidationManager; +import org.briarproject.api.system.Clock; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; + +@Module +public class BlogsModule { + + public static class EagerSingletons { + @Inject + BlogPostValidator blogPostValidator; + } + + @Provides + @Singleton + BlogManager provideBlogManager(BlogManagerImpl blogManager) { + return blogManager; + } + + @Provides + BlogPostFactory provideBlogPostFactory(CryptoComponent crypto, + ClientHelper clientHelper) { + return new BlogPostFactoryImpl(crypto, clientHelper); + } + + @Provides + BlogFactory provideBlogFactory(GroupFactory groupFactory, + AuthorFactory authorFactory, ClientHelper clientHelper) { + return new BlogFactoryImpl(groupFactory, authorFactory, clientHelper); + } + + @Provides + @Singleton + BlogPostValidator provideBlogPostValidator( + ValidationManager validationManager, CryptoComponent crypto, + BlogFactory blogFactory, ClientHelper clientHelper, + MetadataEncoder metadataEncoder, Clock clock) { + + BlogPostValidator validator = new BlogPostValidator(crypto, + blogFactory, clientHelper, metadataEncoder, clock); + validationManager.registerMessageValidator( + BlogManagerImpl.CLIENT_ID, validator); + + return validator; + } + +} diff --git a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java index 29bf817aaea53b313f96dec67837a2ce0e1b9bb9..36f8357bafdfecf063c21a8f270d91271ad24b98 100644 --- a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java +++ b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java @@ -2,7 +2,6 @@ package org.briarproject.forum; import org.briarproject.api.FormatException; import org.briarproject.api.clients.ClientHelper; -import org.briarproject.api.contact.Contact; import org.briarproject.api.data.BdfDictionary; import org.briarproject.api.data.BdfList; import org.briarproject.api.db.DatabaseComponent; @@ -14,8 +13,9 @@ import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumPost; import org.briarproject.api.forum.ForumPostHeader; import org.briarproject.api.identity.Author; +import org.briarproject.api.identity.Author.Status; import org.briarproject.api.identity.AuthorId; -import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.identity.IdentityManager; import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupId; @@ -25,11 +25,9 @@ import org.briarproject.util.StringUtils; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Logger; @@ -45,8 +43,6 @@ import static org.briarproject.api.forum.ForumConstants.KEY_PUBLIC_NAME; import static org.briarproject.api.forum.ForumConstants.KEY_READ; import static org.briarproject.api.forum.ForumConstants.KEY_TIMESTAMP; import static org.briarproject.api.identity.Author.Status.ANONYMOUS; -import static org.briarproject.api.identity.Author.Status.UNKNOWN; -import static org.briarproject.api.identity.Author.Status.VERIFIED; class ForumManagerImpl implements ForumManager { @@ -58,15 +54,17 @@ class ForumManagerImpl implements ForumManager { + "795af837abbf8c16d750b3c2ccc186ea")); private final DatabaseComponent db; + private final IdentityManager identityManager; private final ClientHelper clientHelper; private final ForumFactory forumFactory; private final List<RemoveForumHook> removeHooks; @Inject - ForumManagerImpl(DatabaseComponent db, ClientHelper clientHelper, - ForumFactory forumFactory) { + ForumManagerImpl(DatabaseComponent db, IdentityManager identityManager, + ClientHelper clientHelper, ForumFactory forumFactory) { this.db = db; + this.identityManager = identityManager; this.clientHelper = clientHelper; this.forumFactory = forumFactory; removeHooks = new CopyOnWriteArrayList<RemoveForumHook>(); @@ -183,24 +181,12 @@ class ForumManagerImpl implements ForumManager { @Override public Collection<ForumPostHeader> getPostHeaders(GroupId g) throws DbException { - Set<AuthorId> localAuthorIds = new HashSet<AuthorId>(); - Set<AuthorId> contactAuthorIds = new HashSet<AuthorId>(); + Map<MessageId, BdfDictionary> metadata; - Transaction txn = db.startTransaction(true); try { - // Load the IDs of the user's identities - for (LocalAuthor a : db.getLocalAuthors(txn)) - localAuthorIds.add(a.getId()); - // Load the IDs of contacts' identities - for (Contact c : db.getContacts(txn)) - contactAuthorIds.add(c.getAuthor().getId()); - // Load the metadata - metadata = clientHelper.getMessageMetadataAsDictionary(txn, g); - txn.setComplete(); + metadata = clientHelper.getMessageMetadataAsDictionary(g); } catch (FormatException e) { throw new DbException(e); - } finally { - db.endTransaction(txn); } // Parse the metadata Collection<ForumPostHeader> headers = new ArrayList<ForumPostHeader>(); @@ -209,7 +195,7 @@ class ForumManagerImpl implements ForumManager { BdfDictionary meta = entry.getValue(); long timestamp = meta.getLong(KEY_TIMESTAMP); Author author = null; - Author.Status authorStatus = ANONYMOUS; + Status authorStatus = ANONYMOUS; MessageId parentId = null; if (meta.containsKey(KEY_PARENT)) parentId = new MessageId(meta.getRaw(KEY_PARENT)); @@ -219,11 +205,8 @@ class ForumManagerImpl implements ForumManager { String name = d1.getString(KEY_NAME); byte[] publicKey = d1.getRaw(KEY_PUBLIC_NAME); author = new Author(authorId, name, publicKey); - if (localAuthorIds.contains(authorId)) - authorStatus = VERIFIED; - else if (contactAuthorIds.contains(authorId)) - authorStatus = VERIFIED; - else authorStatus = UNKNOWN; + authorStatus = + identityManager.getAuthorStatus(author.getId()); } String contentType = meta.getString(KEY_CONTENT_TYPE); boolean read = meta.getBoolean(KEY_READ); diff --git a/briar-core/src/org/briarproject/forum/ForumModule.java b/briar-core/src/org/briarproject/forum/ForumModule.java index e7494850e2f95c88aa851ec6abc60d8d4ad770f5..59f56af2acf07df4a8595430239f8cf8f541cc13 100644 --- a/briar-core/src/org/briarproject/forum/ForumModule.java +++ b/briar-core/src/org/briarproject/forum/ForumModule.java @@ -11,6 +11,7 @@ import org.briarproject.api.forum.ForumManager; import org.briarproject.api.forum.ForumPostFactory; import org.briarproject.api.forum.ForumSharingManager; import org.briarproject.api.identity.AuthorFactory; +import org.briarproject.api.identity.IdentityManager; import org.briarproject.api.lifecycle.LifecycleManager; import org.briarproject.api.sync.GroupFactory; import org.briarproject.api.sync.ValidationManager; @@ -38,9 +39,8 @@ public class ForumModule { @Provides @Singleton - ForumManager provideForumManager(DatabaseComponent db, - ClientHelper clientHelper, ForumFactory forumFactory) { - return new ForumManagerImpl(db, clientHelper, forumFactory); + ForumManager provideForumManager(ForumManagerImpl forumManager) { + return forumManager; } @Provides @@ -88,7 +88,7 @@ public class ForumModule { LifecycleManager lifecycleManager, ContactManager contactManager, MessageQueueManager messageQueueManager, - ForumManager forumManager, ForumFactory forumFactory, + ForumManager forumManager, ForumSharingManagerImpl forumSharingManager) { lifecycleManager.registerClient(forumSharingManager); diff --git a/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java b/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java index b3d7a1f4d8213f8427963457ff8a3d80b8c91a0f..e04e206020edd797cf1da082d7b075df9a7156c3 100644 --- a/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java +++ b/briar-core/src/org/briarproject/identity/IdentityManagerImpl.java @@ -1,18 +1,25 @@ package org.briarproject.identity; +import org.briarproject.api.contact.Contact; import org.briarproject.api.db.DatabaseComponent; import org.briarproject.api.db.DbException; import org.briarproject.api.db.Transaction; +import org.briarproject.api.identity.Author.Status; import org.briarproject.api.identity.AuthorId; import org.briarproject.api.identity.IdentityManager; import org.briarproject.api.identity.LocalAuthor; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import javax.inject.Inject; +import static org.briarproject.api.identity.Author.Status.UNKNOWN; +import static org.briarproject.api.identity.Author.Status.VERIFIED; + class IdentityManagerImpl implements IdentityManager { private final DatabaseComponent db; private final List<AddIdentityHook> addHooks; @@ -87,4 +94,22 @@ class IdentityManagerImpl implements IdentityManager { db.endTransaction(txn); } } + + @Override + public Status getAuthorStatus(AuthorId authorId) throws DbException { + Transaction txn = db.startTransaction(false); + try { + // Compare to the IDs of the user's identities + for (LocalAuthor a : db.getLocalAuthors(txn)) + if (a.getId().equals(authorId)) return VERIFIED; + // Compare to the IDs of contacts' identities + for (Contact c : db.getContacts(txn)) + if (c.getAuthor().getId().equals(authorId)) return VERIFIED; + + // TODO also handle UNVERIFIED when #261 is implemented + return UNKNOWN; + } finally { + db.endTransaction(txn); + } + } }