diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.java b/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.java index bf81001b9..a99c2bdbf 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.java +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/HeadlessModule.java @@ -22,6 +22,7 @@ import org.briarproject.bramble.plugin.tor.CircumventionProvider; import org.briarproject.bramble.plugin.tor.LinuxTorPluginFactory; import org.briarproject.bramble.system.JavaSystemModule; import org.briarproject.bramble.util.StringUtils; +import org.briarproject.briar.headless.messaging.MessagingModule; import java.io.File; import java.security.GeneralSecurityException; @@ -42,7 +43,8 @@ import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_PUBL @Module(includes = { JavaNetworkModule.class, JavaSystemModule.class, - CircumventionModule.class + CircumventionModule.class, + MessagingModule.class }) public class HeadlessModule { @@ -119,6 +121,14 @@ public class HeadlessModule { return devConfig; } + @Provides + @Singleton + WebSocketController provideWebSocketHandler( + WebSocketControllerImpl webSocketController) { + return webSocketController; + } + + private File appDir(ConfigurationManager configurationManager, String file) { return new File(configurationManager.getAppDir(), file); diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/Router.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/Router.kt index de23453c2..b743f8af2 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/Router.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/Router.kt @@ -5,7 +5,6 @@ import io.javalin.JavalinEvent.SERVER_START_FAILED import io.javalin.JavalinEvent.SERVER_STOPPED import io.javalin.apibuilder.ApiBuilder.* import org.briarproject.briar.headless.blogs.BlogController -import org.briarproject.briar.headless.contact.ContactController import org.briarproject.briar.headless.forums.ForumController import org.briarproject.briar.headless.messaging.MessagingController import java.lang.Runtime.getRuntime @@ -19,7 +18,8 @@ import kotlin.system.exitProcess class Router @Inject constructor( private val briarService: BriarService, - private val contactController: ContactController, + private val webSocketController: WebSocketController, + private val contactController: MessagingController, private val messagingController: MessagingController, private val forumController: ForumController, private val blogController: BlogController @@ -40,24 +40,30 @@ constructor( .start() app.routes { - path("/contacts") { - get { ctx -> contactController.list(ctx) } - } - path("/messages/:contactId") { - get { ctx -> messagingController.list(ctx) } - post { ctx -> messagingController.write(ctx) } - } - path("/forums") { - get { ctx -> forumController.list(ctx) } - post { ctx -> forumController.create(ctx) } - } - path("/blogs") { - path("/posts") { - get { ctx -> blogController.listPosts(ctx) } - post { ctx -> blogController.createPost(ctx) } + path("/v1") { + path("/contacts") { + get { ctx -> contactController.list(ctx) } + } + path("/messages/:contactId") { + get { ctx -> messagingController.list(ctx) } + post { ctx -> messagingController.write(ctx) } + } + path("/forums") { + get { ctx -> forumController.list(ctx) } + post { ctx -> forumController.create(ctx) } + } + path("/blogs") { + path("/posts") { + get { ctx -> blogController.listPosts(ctx) } + post { ctx -> blogController.createPost(ctx) } + } } } } + app.ws("/v1/ws") { ws -> + ws.onConnect { session -> webSocketController.sessions.add(session) } + ws.onClose { session, _, _ -> webSocketController.sessions.remove(session) } + } } private fun stop() { diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/WebSocketController.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/WebSocketController.kt new file mode 100644 index 000000000..a4abbe86c --- /dev/null +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/WebSocketController.kt @@ -0,0 +1,11 @@ +package org.briarproject.briar.headless + +import io.javalin.websocket.WsSession + +interface WebSocketController { + + val sessions: MutableSet + + fun sendEvent(name: String, obj: Any) + +} diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/WebSocketControllerImpl.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/WebSocketControllerImpl.kt new file mode 100644 index 000000000..c196f18ee --- /dev/null +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/WebSocketControllerImpl.kt @@ -0,0 +1,30 @@ +package org.briarproject.briar.headless + +import io.javalin.json.JavalinJson +import io.javalin.websocket.WsSession +import java.util.concurrent.ConcurrentHashMap +import javax.annotation.concurrent.Immutable +import javax.inject.Inject +import javax.inject.Singleton + +@Immutable +@Singleton +internal class WebSocketControllerImpl @Inject constructor() : WebSocketController { + + override val sessions: MutableSet = ConcurrentHashMap.newKeySet() + + override fun sendEvent(name: String, obj: Any) { + sessions.forEach { session -> + val event = OutputEvent(name, obj) + val json = JavalinJson.toJsonMapper.map(event) + session.send(json) + } + } + +} + +@Immutable +@Suppress("unused") +internal class OutputEvent(val name: String, val data: Any) { + val type = "event" +} diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/Extensions.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/Extensions.kt index f2d5bc248..c23dbee7b 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/Extensions.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/Extensions.kt @@ -1,8 +1,15 @@ package org.briarproject.briar.headless.messaging +import org.briarproject.bramble.api.contact.ContactId import org.briarproject.briar.api.messaging.PrivateMessage import org.briarproject.briar.api.messaging.PrivateMessageHeader +import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent -internal fun PrivateMessageHeader.output(body: String) = OutputPrivateMessage(this, body) +internal fun PrivateMessageHeader.output(contactId: ContactId, body: String) = + OutputPrivateMessage(this, contactId, body) -internal fun PrivateMessage.output(body: String) = OutputPrivateMessage(this, body) +internal fun PrivateMessage.output(contactId: ContactId, body: String) = + OutputPrivateMessage(this, contactId, body) + +internal fun PrivateMessageReceivedEvent.output(body: String) = + messageHeader.output(contactId, body) diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingController.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingController.kt index d9fca98fb..978da8147 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingController.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingController.kt @@ -1,66 +1,11 @@ package org.briarproject.briar.headless.messaging -import io.javalin.BadRequestResponse import io.javalin.Context -import io.javalin.NotFoundResponse -import org.briarproject.bramble.api.contact.Contact -import org.briarproject.bramble.api.contact.ContactId -import org.briarproject.bramble.api.contact.ContactManager -import org.briarproject.bramble.api.db.NoSuchContactException -import org.briarproject.bramble.api.system.Clock -import org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH -import org.briarproject.briar.api.messaging.MessagingManager -import org.briarproject.briar.api.messaging.PrivateMessageFactory -import javax.annotation.concurrent.Immutable -import javax.inject.Inject -import javax.inject.Singleton -@Immutable -@Singleton -class MessagingController @Inject -constructor( - private val messagingManager: MessagingManager, - private val privateMessageFactory: PrivateMessageFactory, - private val contactManager: ContactManager, - private val clock: Clock -) { +interface MessagingController { - fun list(ctx: Context): Context { - val contact = getContact(ctx) + fun list(ctx: Context): Context - val messages = messagingManager.getMessageHeaders(contact.id).map { header -> - val body = messagingManager.getMessageBody(header.id) - header.output(body) - } - return ctx.json(messages) - } - - fun write(ctx: Context): Context { - val contact = getContact(ctx) - - val message = ctx.formParam("message") - if (message == null || message.isEmpty()) - throw BadRequestResponse("Expecting Message text") - if (message.length > MAX_PRIVATE_MESSAGE_BODY_LENGTH) - throw BadRequestResponse("Message text too large") - - val group = messagingManager.getContactGroup(contact) - val now = clock.currentTimeMillis() - val m = privateMessageFactory.createPrivateMessage(group.id, now, message) - - messagingManager.addLocalMessage(m) - return ctx.json(m.output(message)) - } - - private fun getContact(ctx: Context): Contact { - val contactString = ctx.pathParam("contactId") - val contactInt = Integer.parseInt(contactString) - val contactId = ContactId(contactInt) - return try { - contactManager.getContact(contactId) - } catch (e: NoSuchContactException) { - throw NotFoundResponse() - } - } + fun write(ctx: Context): Context } diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingControllerImpl.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingControllerImpl.kt new file mode 100644 index 000000000..952fc77d6 --- /dev/null +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingControllerImpl.kt @@ -0,0 +1,85 @@ +package org.briarproject.briar.headless.messaging + +import io.javalin.BadRequestResponse +import io.javalin.Context +import io.javalin.NotFoundResponse +import org.briarproject.bramble.api.contact.Contact +import org.briarproject.bramble.api.contact.ContactId +import org.briarproject.bramble.api.contact.ContactManager +import org.briarproject.bramble.api.db.DatabaseExecutor +import org.briarproject.bramble.api.db.NoSuchContactException +import org.briarproject.bramble.api.event.Event +import org.briarproject.bramble.api.event.EventListener +import org.briarproject.bramble.api.system.Clock +import org.briarproject.briar.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH +import org.briarproject.briar.api.messaging.MessagingManager +import org.briarproject.briar.api.messaging.PrivateMessageFactory +import org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent +import org.briarproject.briar.headless.WebSocketController +import java.util.concurrent.Executor +import javax.annotation.concurrent.Immutable +import javax.inject.Inject +import javax.inject.Singleton + +@Immutable +@Singleton +internal class MessagingControllerImpl @Inject +constructor( + private val messagingManager: MessagingManager, + private val privateMessageFactory: PrivateMessageFactory, + private val contactManager: ContactManager, + private val webSocketController: WebSocketController, + @DatabaseExecutor private val dbExecutor: Executor, + private val clock: Clock +) : MessagingController, EventListener { + + override fun list(ctx: Context): Context { + val contact = getContact(ctx) + + val messages = messagingManager.getMessageHeaders(contact.id).map { header -> + val body = messagingManager.getMessageBody(header.id) + header.output(contact.id, body) + } + return ctx.json(messages) + } + + override fun write(ctx: Context): Context { + val contact = getContact(ctx) + + val message = ctx.formParam("message") + if (message == null || message.isEmpty()) + throw BadRequestResponse("Expecting Message text") + if (message.length > MAX_PRIVATE_MESSAGE_BODY_LENGTH) + throw BadRequestResponse("Message text too large") + + val group = messagingManager.getContactGroup(contact) + val now = clock.currentTimeMillis() + val m = privateMessageFactory.createPrivateMessage(group.id, now, message) + + messagingManager.addLocalMessage(m) + return ctx.json(m.output(contact.id, message)) + } + + override fun eventOccurred(e: Event) { + when (e) { + is PrivateMessageReceivedEvent -> dbExecutor.run { + val name = + "org.briarproject.briar.api.messaging.event.PrivateMessageReceivedEvent" + val body = messagingManager.getMessageBody(e.messageHeader.id) + webSocketController.sendEvent(name, e.output(body)) + } + } + } + + private fun getContact(ctx: Context): Contact { + val contactString = ctx.pathParam("contactId") + val contactInt = Integer.parseInt(contactString) + val contactId = ContactId(contactInt) + return try { + contactManager.getContact(contactId) + } catch (e: NoSuchContactException) { + throw NotFoundResponse() + } + } + +} diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingModule.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingModule.kt new file mode 100644 index 000000000..a334380f6 --- /dev/null +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/MessagingModule.kt @@ -0,0 +1,20 @@ +package org.briarproject.briar.headless.messaging + +import dagger.Module +import dagger.Provides +import org.briarproject.bramble.api.event.EventBus +import javax.inject.Singleton + +@Module +class MessagingModule { + + @Provides + @Singleton + internal fun provideMessagingController( + eventBus: EventBus, messagingController: MessagingControllerImpl + ): MessagingController { + eventBus.addListener(messagingController) + return messagingController + } + +} diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/OutputPrivateMessage.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/OutputPrivateMessage.kt index 1b93256ee..53a861595 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/OutputPrivateMessage.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/messaging/OutputPrivateMessage.kt @@ -1,5 +1,6 @@ package org.briarproject.briar.headless.messaging +import org.briarproject.bramble.api.contact.ContactId import org.briarproject.briar.api.messaging.PrivateMessage import org.briarproject.briar.api.messaging.PrivateMessageHeader import javax.annotation.concurrent.Immutable @@ -16,8 +17,9 @@ internal class OutputPrivateMessage { val local: Boolean val id: ByteArray val groupId: ByteArray + val contactId: Int - internal constructor(header: PrivateMessageHeader, body: String) { + internal constructor(header: PrivateMessageHeader, contactId: ContactId, body: String) { this.body = body this.timestamp = header.timestamp this.read = header.isRead @@ -26,12 +28,13 @@ internal class OutputPrivateMessage { this.local = header.isLocal this.id = header.id.bytes this.groupId = header.groupId.bytes + this.contactId = contactId.int } /** * Only meant for own [PrivateMessage]s directly after creation. */ - internal constructor(m: PrivateMessage, body: String) { + internal constructor(m: PrivateMessage, contactId: ContactId, body: String) { this.body = body this.timestamp = m.message.timestamp this.read = true @@ -40,5 +43,6 @@ internal class OutputPrivateMessage { this.local = true this.id = m.message.id.bytes this.groupId = m.message.groupId.bytes + this.contactId = contactId.int } }