diff --git a/briar-headless/README.md b/briar-headless/README.md index 8dd09ac35..4a00e97ab 100644 --- a/briar-headless/README.md +++ b/briar-headless/README.md @@ -68,7 +68,8 @@ Returns a JSON array of contacts: "alias" : "A local nickname", "handshakePublicKey": "XnYRd7a7E4CTqgAvh4hCxh/YZ0EPscxknB9ZcEOpSzY=", "verified": true, - "lastChatActivity": 1557838312175 + "lastChatActivity": 1557838312175, + "connected": false } ``` @@ -392,3 +393,19 @@ will no longer work on making this `pendingContact` become `contact`. "type": "event" } ``` + +### A contact connected or disconnected + +When Briar establishes a connection to a contact (the contact comes online), +it sends a `ContactConnectedEvent`. +When the last connection is lost (the contact goes offline), it sends a `ContactDisconnectedEvent`. + +```json +{ + "data": { + "contactId": 1 + }, + "name": "ContactConnectedEvent", + "type": "event" +} +``` diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/ContactControllerImpl.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/ContactControllerImpl.kt index 70f7f6cb6..4cffe0d96 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/ContactControllerImpl.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/ContactControllerImpl.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.javalin.http.BadRequestResponse import io.javalin.http.Context import io.javalin.http.NotFoundResponse +import org.briarproject.bramble.api.connection.ConnectionRegistry import org.briarproject.bramble.api.contact.ContactManager import org.briarproject.bramble.api.contact.HandshakeLinkConstants.LINK_REGEX import org.briarproject.bramble.api.contact.PendingContactId @@ -16,6 +17,8 @@ import org.briarproject.bramble.api.db.NoSuchPendingContactException import org.briarproject.bramble.api.event.Event import org.briarproject.bramble.api.event.EventListener import org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH +import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent +import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent import org.briarproject.bramble.util.StringUtils.toUtf8 import org.briarproject.briar.api.conversation.ConversationManager import org.briarproject.briar.headless.event.WebSocketController @@ -32,6 +35,8 @@ internal const val EVENT_CONTACT_ADDED = "ContactAddedEvent" internal const val EVENT_PENDING_CONTACT_STATE_CHANGED = "PendingContactStateChangedEvent" internal const val EVENT_PENDING_CONTACT_ADDED = "PendingContactAddedEvent" internal const val EVENT_PENDING_CONTACT_REMOVED = "PendingContactRemovedEvent" +internal const val EVENT_CONTACT_CONNECTED = "ContactConnectedEvent" +internal const val EVENT_CONTACT_DISCONNECTED = "ContactDisconnectedEvent" @Immutable @Singleton @@ -41,7 +46,8 @@ constructor( private val contactManager: ContactManager, private val conversationManager: ConversationManager, private val objectMapper: ObjectMapper, - private val webSocket: WebSocketController + private val webSocket: WebSocketController, + private val connectionRegistry: ConnectionRegistry ) : ContactController, EventListener { override fun eventOccurred(e: Event) = when (e) { @@ -57,6 +63,12 @@ constructor( is PendingContactRemovedEvent -> { webSocket.sendEvent(EVENT_PENDING_CONTACT_REMOVED, e.output()) } + is ContactConnectedEvent -> { + webSocket.sendEvent(EVENT_CONTACT_CONNECTED, e.output()) + } + is ContactDisconnectedEvent -> { + webSocket.sendEvent(EVENT_CONTACT_DISCONNECTED, e.output()) + } else -> { } } @@ -64,7 +76,8 @@ constructor( override fun list(ctx: Context): Context { val contacts = contactManager.contacts.map { contact -> val latestMsgTime = conversationManager.getGroupCount(contact.id).latestMsgTime - contact.output(latestMsgTime) + val connected = connectionRegistry.isConnected(contact.id) + contact.output(latestMsgTime, connected) } return ctx.json(contacts) } diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputContact.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputContact.kt index 9954edfda..2d8656047 100644 --- a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputContact.kt +++ b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputContact.kt @@ -2,15 +2,17 @@ package org.briarproject.briar.headless.contact import org.briarproject.bramble.api.contact.Contact import org.briarproject.bramble.api.contact.event.ContactAddedEvent +import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent +import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent import org.briarproject.bramble.identity.output -import org.briarproject.briar.api.conversation.ConversationManager import org.briarproject.briar.headless.json.JsonDict -internal fun Contact.output(latestMsgTime: Long) = JsonDict( +internal fun Contact.output(latestMsgTime: Long, connected: Boolean) = JsonDict( "contactId" to id.int, "author" to author.output(), "verified" to isVerified, - "lastChatActivity" to latestMsgTime + "lastChatActivity" to latestMsgTime, + "connected" to connected ).apply { alias?.let { put("alias", it) } handshakePublicKey?.let { put("handshakePublicKey", it.encoded) } @@ -20,3 +22,11 @@ internal fun ContactAddedEvent.output() = JsonDict( "contactId" to contactId.int, "verified" to isVerified ) + +internal fun ContactConnectedEvent.output() = JsonDict( + "contactId" to contactId.int +) + +internal fun ContactDisconnectedEvent.output() = JsonDict( + "contactId" to contactId.int +) diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/ControllerTest.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/ControllerTest.kt index c73f45272..7e16d71af 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/ControllerTest.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/ControllerTest.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.javalin.http.Context import io.javalin.http.util.ContextUtil import io.mockk.mockk +import org.briarproject.bramble.api.connection.ConnectionRegistry import org.briarproject.bramble.api.contact.Contact import org.briarproject.bramble.api.contact.ContactManager import org.briarproject.bramble.api.identity.Author @@ -26,6 +27,7 @@ abstract class ControllerTest { protected val contactManager = mockk() protected val conversationManager = mockk() protected val identityManager = mockk() + protected val connectionRegistry = mockk() protected val clock = mockk() protected val ctx = mockk() diff --git a/briar-headless/src/test/java/org/briarproject/briar/headless/contact/ContactControllerTest.kt b/briar-headless/src/test/java/org/briarproject/briar/headless/contact/ContactControllerTest.kt index 3f3220d18..59ee3c06a 100644 --- a/briar-headless/src/test/java/org/briarproject/briar/headless/contact/ContactControllerTest.kt +++ b/briar-headless/src/test/java/org/briarproject/briar/headless/contact/ContactControllerTest.kt @@ -20,6 +20,8 @@ import org.briarproject.bramble.api.contact.event.PendingContactStateChangedEven import org.briarproject.bramble.api.db.NoSuchContactException import org.briarproject.bramble.api.db.NoSuchPendingContactException import org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH +import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent +import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent import org.briarproject.bramble.identity.output import org.briarproject.bramble.test.TestUtils.getPendingContact import org.briarproject.bramble.test.TestUtils.getRandomBytes @@ -29,6 +31,7 @@ import org.briarproject.briar.headless.json.JsonDict import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test +import kotlin.random.Random internal class ContactControllerTest : ControllerTest() { @@ -38,7 +41,8 @@ internal class ContactControllerTest : ControllerTest() { contactManager, conversationManager, objectMapper, - webSocketController + webSocketController, + connectionRegistry ) @Test @@ -50,9 +54,11 @@ internal class ContactControllerTest : ControllerTest() { @Test fun testList() { + val connected = Random.nextBoolean() every { contactManager.contacts } returns listOf(contact) every { conversationManager.getGroupCount(contact.id).latestMsgTime } returns timestamp - every { ctx.json(listOf(contact.output(timestamp))) } returns ctx + every { connectionRegistry.isConnected(contact.id) } returns connected + every { ctx.json(listOf(contact.output(timestamp, connected))) } returns ctx controller.list(ctx) } @@ -267,8 +273,37 @@ internal class ContactControllerTest : ControllerTest() { controller.eventOccurred(event) } + @Test + fun testContactConnectedEvent() { + val event = ContactConnectedEvent(contact.id) + + every { + webSocketController.sendEvent( + EVENT_CONTACT_CONNECTED, + event.output() + ) + } just runs + + controller.eventOccurred(event) + } + + @Test + fun testContactDisconnectedEvent() { + val event = ContactDisconnectedEvent(contact.id) + + every { + webSocketController.sendEvent( + EVENT_CONTACT_DISCONNECTED, + event.output() + ) + } just runs + + controller.eventOccurred(event) + } + @Test fun testOutputContact() { + val connected = Random.nextBoolean() assertNotNull(contact.handshakePublicKey) val json = """ { @@ -277,10 +312,11 @@ internal class ContactControllerTest : ControllerTest() { "alias" : "${contact.alias}", "handshakePublicKey": ${toJson(contact.handshakePublicKey!!.encoded)}, "verified": ${contact.isVerified}, - "lastChatActivity": $timestamp + "lastChatActivity": $timestamp, + "connected": $connected } """ - assertJsonEquals(json, contact.output(timestamp)) + assertJsonEquals(json, contact.output(timestamp, connected)) } @Test @@ -358,4 +394,26 @@ internal class ContactControllerTest : ControllerTest() { assertJsonEquals(json, event.output()) } + @Test + fun testOutputContactConnectedEvent() { + val event = ContactConnectedEvent(contact.id) + val json = """ + { + "contactId": ${contact.id.int} + } + """ + assertJsonEquals(json, event.output()) + } + + @Test + fun testOutputContactDisconnectedEvent() { + val event = ContactDisconnectedEvent(contact.id) + val json = """ + { + "contactId": ${contact.id.int} + } + """ + assertJsonEquals(json, event.output()) + } + }