Browse Source

Initial pass at labelled-replies support

tags/v0.10.0
Chris Smith 5 years ago
parent
commit
c3908e1a60

+ 2
- 0
CHANGELOG View File

@@ -1,6 +1,8 @@
1 1
 vNEXT (in development)
2 2
 
3 3
  * Batch start and end events are no longer included in BatchReceived events
4
+ * Batches now expose complete metadata from their start event
5
+ * Added support for labelled-replies capability and label message tags
4 6
 
5 7
 v0.9.0
6 8
 

+ 19
- 5
src/main/kotlin/com/dmdirc/ktirc/IrcClientImpl.kt View File

@@ -8,11 +8,9 @@ import com.dmdirc.ktirc.io.LineBufferedSocket
8 8
 import com.dmdirc.ktirc.io.MessageHandler
9 9
 import com.dmdirc.ktirc.io.MessageParser
10 10
 import com.dmdirc.ktirc.messages.*
11
-import com.dmdirc.ktirc.model.ChannelStateMap
12
-import com.dmdirc.ktirc.model.ServerState
13
-import com.dmdirc.ktirc.model.UserState
14
-import com.dmdirc.ktirc.model.toConnectionError
11
+import com.dmdirc.ktirc.model.*
15 12
 import com.dmdirc.ktirc.util.currentTimeProvider
13
+import com.dmdirc.ktirc.util.generateLabel
16 14
 import com.dmdirc.ktirc.util.logger
17 15
 import io.ktor.util.KtorExperimentalAPI
18 16
 import kotlinx.coroutines.*
@@ -52,6 +50,22 @@ internal class IrcClientImpl(private val config: IrcClientConfig) : IrcClient, C
52 50
         socket?.sendChannel?.offer(message.toByteArray()) ?: log.warning { "No send channel for message: $message" }
53 51
     }
54 52
 
53
+    // TODO: This will become sendAsync and return a Deferred<IrcEvent>
54
+    // TODO: Refactor so that send takes a map of tags and arguments; build the string separately
55
+    internal fun sendWithLabel(message: String) {
56
+        val messageToSend = if (Capability.LabeledResponse in serverState.capabilities.enabledCapabilities) {
57
+            val label = generateLabel(this)
58
+            "@draft/label=$label" + if (message.startsWith('@')) {
59
+                ";${message.substring(1)}"
60
+            } else {
61
+                " $message"
62
+            }
63
+        } else {
64
+            message
65
+        }
66
+        socket?.sendChannel?.offer(messageToSend.toByteArray()) ?: log.warning { "No send channel for message: $message" }
67
+    }
68
+
55 69
     override fun connect() {
56 70
         check(!connecting.getAndSet(true))
57 71
 
@@ -70,7 +84,7 @@ internal class IrcClientImpl(private val config: IrcClientConfig) : IrcClient, C
70 84
                     sendNickChange(config.profile.nickname)
71 85
                     sendUser(config.profile.username, config.profile.realName)
72 86
                     messageHandler.processMessages(this@IrcClientImpl, receiveChannel.map { parser.parse(it) })
73
-                } catch (ex : Exception) {
87
+                } catch (ex: Exception) {
74 88
                     emitEvent(ServerConnectionError(EventMetadata(currentTimeProvider()), ex.toConnectionError(), ex.localizedMessage))
75 89
                 }
76 90
 

+ 6
- 1
src/main/kotlin/com/dmdirc/ktirc/events/Events.kt View File

@@ -12,8 +12,13 @@ import java.time.LocalDateTime
12 12
  * @param time The best-guess time at which the event occurred.
13 13
  * @param batchId The ID of the batch this event is part of, if any.
14 14
  * @param messageId The unique ID of this message, if any.
15
+ * @param label The label of the command that this event was sent in response to, if any.
15 16
  */
16
-data class EventMetadata(val time: LocalDateTime, val batchId: String? = null, val messageId: String? = null)
17
+data class EventMetadata(
18
+        val time: LocalDateTime,
19
+        val batchId: String? = null,
20
+        val messageId: String? = null,
21
+        val label: String? = null)
17 22
 
18 23
 /** Base class for all events. */
19 24
 sealed class IrcEvent(val metadata: EventMetadata) {

+ 3
- 3
src/main/kotlin/com/dmdirc/ktirc/events/mutators/BatchMutator.kt View File

@@ -22,16 +22,16 @@ internal class BatchMutator : EventMutator {
22 22
 
23 23
     private fun startBatch(client: IrcClient, event: BatchStarted) {
24 24
         client.serverState.batches[event.referenceId] =
25
-                Batch(event.batchType, event.params.asList(), event.metadata.batchId, mutableListOf())
25
+                Batch(event.batchType, event.params.asList(), event.metadata, mutableListOf())
26 26
     }
27 27
 
28 28
     private fun finishBatch(client: IrcClient, event: BatchFinished): List<IrcEvent> {
29 29
         client.serverState.batches.remove(event.referenceId)?.let {
30 30
             val batch = BatchReceived(it.events[0].metadata, it.type, it.arguments.toTypedArray(), it.events)
31
-            if (it.parent == null) {
31
+            if (it.metadata.batchId == null) {
32 32
                 return listOf(batch)
33 33
             } else {
34
-                client.serverState.batches[it.parent]?.events?.add(batch)
34
+                client.serverState.batches[it.metadata.batchId]?.events?.add(batch)
35 35
             }
36 36
         }
37 37
 

+ 5
- 2
src/main/kotlin/com/dmdirc/ktirc/model/CapabilitiesState.kt View File

@@ -80,11 +80,14 @@ sealed class Capability(val name: String) {
80 80
     object AccountAndRealNameInJoinMessages : Capability("extended-join")
81 81
 
82 82
     // Capabilities that affect how messages are sent/received:
83
+    /** Messages can be sent in batches, and potentially handled differently by the client. */
84
+    object Batch : Capability("batch")
85
+
83 86
     /** Messages sent by the client are echo'd back on successful delivery. */
84 87
     object EchoMessages : Capability("echo-message")
85 88
 
86
-    /** Messages can be sent in batches, and potentially handled differently by the client. */
87
-    object Batch : Capability("batch")
89
+    /** Allows us to label all outgoing messages and have the server identify the responses to them. */
90
+    object LabeledResponse : Capability("draft/labeled-response")
88 91
 
89 92
     // Capabilities that notify us of changes to other clients:
90 93
     /** Receive a notification when a user's account changes. */

+ 10
- 9
src/main/kotlin/com/dmdirc/ktirc/model/IrcMessage.kt View File

@@ -12,7 +12,11 @@ import java.time.LocalDateTime
12 12
 internal class IrcMessage(val tags: Map<MessageTag, String>, val prefix: ByteArray?, val command: String, val params: List<ByteArray>) {
13 13
 
14 14
     /** The time at which the message was sent, or our best guess at it. */
15
-    val metadata = EventMetadata(time, batchId, messageId)
15
+    val metadata = EventMetadata(
16
+            time = time,
17
+            batchId = tags[MessageTag.Batch],
18
+            messageId = tags[MessageTag.MessageId],
19
+            label = tags[MessageTag.Label])
16 20
 
17 21
     /** The user that generated the message, if any. */
18 22
     val sourceUser by lazy {
@@ -27,12 +31,6 @@ internal class IrcMessage(val tags: Map<MessageTag, String>, val prefix: ByteArr
27 31
             false -> currentTimeProvider()
28 32
         }
29 33
 
30
-    private val batchId
31
-        get() = tags[MessageTag.Batch]
32
-
33
-    private val messageId
34
-        get() = tags[MessageTag.MessageId]
35
-
36 34
 }
37 35
 
38 36
 /**
@@ -46,8 +44,8 @@ sealed class MessageTag(val name: String) {
46 44
     /** Specifies the ID that a batch message belongs to. */
47 45
     object Batch : MessageTag("batch")
48 46
 
49
-    /** Specifies the time the server received the message, if the `server-time` capability is negotiated. */
50
-    object ServerTime : MessageTag("time")
47
+    /** An arbitrary label to identify the response to messages we generate. */
48
+    object Label : MessageTag("draft/label")
51 49
 
52 50
     /** A unique ID for the message, used to reply, react, edit, delete, etc. */
53 51
     object MessageId : MessageTag("draft/msgid")
@@ -57,6 +55,9 @@ sealed class MessageTag(val name: String) {
57 55
 
58 56
     /** Used to specify a slack-like reaction to another message. */
59 57
     object React : MessageTag("+draft/react")
58
+
59
+    /** Specifies the time the server received the message, if the `server-time` capability is negotiated. */
60
+    object ServerTime : MessageTag("time")
60 61
 }
61 62
 
62 63
 internal val messageTags: Map<String, MessageTag> by lazy {

+ 9
- 1
src/main/kotlin/com/dmdirc/ktirc/model/ServerState.kt View File

@@ -1,9 +1,11 @@
1 1
 package com.dmdirc.ktirc.model
2 2
 
3 3
 import com.dmdirc.ktirc.SaslConfig
4
+import com.dmdirc.ktirc.events.EventMetadata
4 5
 import com.dmdirc.ktirc.events.IrcEvent
5 6
 import com.dmdirc.ktirc.io.CaseMapping
6 7
 import com.dmdirc.ktirc.util.logger
8
+import java.util.concurrent.atomic.AtomicLong
7 9
 import kotlin.reflect.KClass
8 10
 
9 11
 /**
@@ -69,6 +71,11 @@ class ServerState internal constructor(
69 71
      */
70 72
     internal val batches = mutableMapOf<String, Batch>()
71 73
 
74
+    /**
75
+     * Counter for ensuring sent labels are unique.
76
+     */
77
+    internal val labelCounter = AtomicLong(0)
78
+
72 79
     /**
73 80
      * Determines if the given mode is one applied to a user of a channel, such as 'o' for operator.
74 81
      */
@@ -99,6 +106,7 @@ class ServerState internal constructor(
99 106
         capabilities.reset()
100 107
         sasl.reset()
101 108
         batches.clear()
109
+        labelCounter.set(0)
102 110
     }
103 111
 
104 112
 }
@@ -186,7 +194,7 @@ enum class ServerStatus {
186 194
 /**
187 195
  * Represents an in-progress batch.
188 196
  */
189
-internal data class Batch(val type: String, val arguments: List<String>, val parent: String? = null, val events: MutableList<IrcEvent> = mutableListOf())
197
+internal data class Batch(val type: String, val arguments: List<String>, val metadata: EventMetadata, val events: MutableList<IrcEvent> = mutableListOf())
190 198
 
191 199
 internal val serverFeatures: Map<String, ServerFeature<*>> by lazy {
192 200
     ServerFeature::class.nestedClasses.map { it.objectInstance as ServerFeature<*> }.associateBy { it.name }

+ 16
- 0
src/main/kotlin/com/dmdirc/ktirc/util/Labels.kt View File

@@ -0,0 +1,16 @@
1
+package com.dmdirc.ktirc.util
2
+
3
+import com.dmdirc.ktirc.IrcClient
4
+import com.dmdirc.ktirc.sasl.toBase64
5
+import java.time.ZoneOffset
6
+
7
+internal var generateLabel = { ircClient: IrcClient ->
8
+    val time = currentTimeProvider().toEpochSecond(ZoneOffset.UTC)
9
+    val counter = ircClient.serverState.labelCounter.incrementAndGet()
10
+    ByteArray(6) {
11
+        when {
12
+            it < 3 -> ((time shr it) and 0xff).toByte()
13
+            else -> ((counter shr (it - 3)) and 0xff).toByte()
14
+        }
15
+    }.toBase64()
16
+}

+ 52
- 14
src/test/kotlin/com/dmdirc/ktirc/IrcClientImplTest.kt View File

@@ -3,11 +3,9 @@ package com.dmdirc.ktirc
3 3
 import com.dmdirc.ktirc.events.*
4 4
 import com.dmdirc.ktirc.io.CaseMapping
5 5
 import com.dmdirc.ktirc.io.LineBufferedSocket
6
-import com.dmdirc.ktirc.model.ChannelState
7
-import com.dmdirc.ktirc.model.ConnectionError
8
-import com.dmdirc.ktirc.model.ServerFeature
9
-import com.dmdirc.ktirc.model.User
6
+import com.dmdirc.ktirc.model.*
10 7
 import com.dmdirc.ktirc.util.currentTimeProvider
8
+import com.dmdirc.ktirc.util.generateLabel
11 9
 import com.nhaarman.mockitokotlin2.*
12 10
 import io.ktor.util.KtorExperimentalAPI
13 11
 import kotlinx.coroutines.*
@@ -198,16 +196,44 @@ internal class IrcClientImplTest {
198 196
 
199 197
         client.send("testing 123")
200 198
 
201
-        assertEquals(true, withTimeoutOrNull(500) {
202
-            var found = false
203
-            for (line in sendLineChannel) {
204
-                if (String(line) == "testing 123") {
205
-                    found = true
206
-                    break
207
-                }
208
-            }
209
-            found
210
-        })
199
+        assertLineReceived("testing 123")
200
+    }
201
+
202
+    @Test
203
+    fun `sends text to socket without label if cap is missing`() = runBlocking {
204
+        val client = IrcClientImpl(normalConfig)
205
+        client.socketFactory = mockSocketFactory
206
+        client.connect()
207
+
208
+        client.sendWithLabel("testing 123")
209
+
210
+        assertLineReceived("testing 123")
211
+    }
212
+
213
+    @Test
214
+    fun `sends text to socket with added tags and label`() = runBlocking {
215
+        generateLabel = { "abc123" }
216
+        val client = IrcClientImpl(normalConfig)
217
+        client.socketFactory = mockSocketFactory
218
+        client.serverState.capabilities.enabledCapabilities[Capability.LabeledResponse] = ""
219
+        client.connect()
220
+
221
+        client.sendWithLabel("testing 123")
222
+
223
+        assertLineReceived("@draft/label=abc123 testing 123")
224
+    }
225
+
226
+    @Test
227
+    fun `sends tagged text to socket with label`() = runBlocking {
228
+        generateLabel = { "abc123" }
229
+        val client = IrcClientImpl(normalConfig)
230
+        client.socketFactory = mockSocketFactory
231
+        client.serverState.capabilities.enabledCapabilities[Capability.LabeledResponse] = ""
232
+        client.connect()
233
+
234
+        client.sendWithLabel("@+test=x testing 123")
235
+
236
+        assertLineReceived("@draft/label=abc123;+test=x testing 123")
211 237
     }
212 238
 
213 239
     @Test
@@ -338,5 +364,17 @@ internal class IrcClientImplTest {
338 364
         return value.get()
339 365
     }
340 366
 
367
+    private suspend fun assertLineReceived(expected: String) {
368
+        assertEquals(true, withTimeoutOrNull(500) {
369
+            for (line in sendLineChannel.map { String(it) }) {
370
+                println(line)
371
+                if (line == expected) {
372
+                    return@withTimeoutOrNull true
373
+                }
374
+            }
375
+            false
376
+        }) { "Expected to receive $expected" }
377
+    }
378
+
341 379
 
342 380
 }

+ 8
- 8
src/test/kotlin/com/dmdirc/ktirc/events/mutators/BatchMutatorTest.kt View File

@@ -41,13 +41,13 @@ internal class BatchMutatorTest {
41 41
             assertEquals(listOf("foo", "bar"), it.arguments)
42 42
             assertEquals("netsplit", it.type)
43 43
             assertTrue(it.events.isEmpty())
44
-            assertNull(it.parent)
44
+            assertNull(it.metadata.batchId)
45 45
         }
46 46
     }
47 47
 
48 48
     @Test
49 49
     fun `adds to batch when event has a batch ID`() {
50
-        serverState.batches["abcdef"] = Batch("netsplit", emptyList())
50
+        serverState.batches["abcdef"] = Batch("netsplit", emptyList(), EventMetadata(TestConstants.time))
51 51
 
52 52
         val event = UserNickChanged(EventMetadata(TestConstants.time, "abcdef"), User("zeroCool"), "crashOverride")
53 53
         mutator.mutateEvent(ircClient, messageEmitter, event)
@@ -57,7 +57,7 @@ internal class BatchMutatorTest {
57 57
 
58 58
     @Test
59 59
     fun `suppresses event when it has a batch ID`() {
60
-        serverState.batches["abcdef"] = Batch("netsplit", emptyList())
60
+        serverState.batches["abcdef"] = Batch("netsplit", emptyList(), EventMetadata(TestConstants.time))
61 61
 
62 62
         val event = UserNickChanged(EventMetadata(TestConstants.time, "abcdef"), User("zeroCool"), "crashOverride")
63 63
         val events = mutator.mutateEvent(ircClient, messageEmitter, event)
@@ -67,7 +67,7 @@ internal class BatchMutatorTest {
67 67
 
68 68
     @Test
69 69
     fun `passes event for processing only when it has a batch ID`() {
70
-        serverState.batches["abcdef"] = Batch("netsplit", emptyList())
70
+        serverState.batches["abcdef"] = Batch("netsplit", emptyList(), EventMetadata(TestConstants.time))
71 71
 
72 72
         val event = UserNickChanged(EventMetadata(TestConstants.time, "abcdef"), User("zeroCool"), "crashOverride")
73 73
         mutator.mutateEvent(ircClient, messageEmitter, event)
@@ -77,7 +77,7 @@ internal class BatchMutatorTest {
77 77
 
78 78
     @Test
79 79
     fun `sends a batch when it finishes and the parent is null`() {
80
-        serverState.batches["abcdef"] = Batch("netsplit", listOf("p1", "p2"), events = mutableListOf(ServerConnected(EventMetadata(TestConstants.time, "abcdef"))))
80
+        serverState.batches["abcdef"] = Batch("netsplit", listOf("p1", "p2"), EventMetadata(TestConstants.time), events = mutableListOf(ServerConnected(EventMetadata(TestConstants.time, "abcdef"))))
81 81
 
82 82
         val events = mutator.mutateEvent(ircClient, messageEmitter, BatchFinished(EventMetadata(TestConstants.time), "abcdef"))
83 83
 
@@ -92,8 +92,8 @@ internal class BatchMutatorTest {
92 92
 
93 93
     @Test
94 94
     fun `adds a batch to its parent when it finishes`() {
95
-        serverState.batches["12345"] = Batch("history", emptyList())
96
-        serverState.batches["abcdef"] = Batch("netsplit", emptyList(), "12345", mutableListOf(ServerConnected(EventMetadata(TestConstants.time, "abcdef"))))
95
+        serverState.batches["12345"] = Batch("history", emptyList(), EventMetadata(TestConstants.time))
96
+        serverState.batches["abcdef"] = Batch("netsplit", emptyList(), EventMetadata(TestConstants.time, batchId = "12345"), mutableListOf(ServerConnected(EventMetadata(TestConstants.time, "abcdef"))))
97 97
 
98 98
         val events = mutator.mutateEvent(ircClient, messageEmitter, BatchFinished(EventMetadata(TestConstants.time), "abcdef"))
99 99
 
@@ -109,7 +109,7 @@ internal class BatchMutatorTest {
109 109
 
110 110
     @Test
111 111
     fun `deletes batch when it finishes`() {
112
-        serverState.batches["abcdef"] = Batch("netsplit", emptyList(), events = mutableListOf(ServerConnected(EventMetadata(TestConstants.time, "abcdef"))))
112
+        serverState.batches["abcdef"] = Batch("netsplit", emptyList(), EventMetadata(TestConstants.time), events = mutableListOf(ServerConnected(EventMetadata(TestConstants.time, "abcdef"))))
113 113
 
114 114
         mutator.mutateEvent(ircClient, messageEmitter, BatchFinished(EventMetadata(TestConstants.time), "abcdef"))
115 115
 

+ 7
- 0
src/test/kotlin/com/dmdirc/ktirc/model/IrcMessageTest.kt View File

@@ -46,6 +46,13 @@ internal class IrcMessageTest {
46 46
         assertEquals("abc123", message.metadata.messageId)
47 47
     }
48 48
 
49
+    @Test
50
+    fun `populates label if present`() {
51
+        currentTimeProvider = { TestConstants.time }
52
+        val message = IrcMessage(hashMapOf(MessageTag.Label to "abc123"), null, "", emptyList())
53
+        assertEquals("abc123", message.metadata.label)
54
+    }
55
+
49 56
     @Test
50 57
     fun `Can parse the prefix as a source user`() {
51 58
         val message = IrcMessage(emptyMap(), "acidBurn!libby@root.localhost".toByteArray(), "", emptyList())

+ 5
- 1
src/test/kotlin/com/dmdirc/ktirc/model/ServerStateTest.kt View File

@@ -1,5 +1,7 @@
1 1
 package com.dmdirc.ktirc.model
2 2
 
3
+import com.dmdirc.ktirc.TestConstants
4
+import com.dmdirc.ktirc.events.EventMetadata
3 5
 import org.junit.jupiter.api.Assertions.*
4 6
 import org.junit.jupiter.api.Test
5 7
 
@@ -67,7 +69,8 @@ internal class ServerStateTest {
67 69
         features[ServerFeature.Network] = "gibson"
68 70
         capabilities.advertisedCapabilities[Capability.SaslAuthentication] = "sure"
69 71
         sasl.saslBuffer = "in progress"
70
-        batches["batch"] = Batch("type", emptyList())
72
+        batches["batch"] = Batch("type", emptyList(), EventMetadata(TestConstants.time))
73
+        labelCounter.set(100)
71 74
 
72 75
         reset()
73 76
 
@@ -79,6 +82,7 @@ internal class ServerStateTest {
79 82
         assertTrue(capabilities.advertisedCapabilities.isEmpty())
80 83
         assertEquals("", sasl.saslBuffer)
81 84
         assertTrue(batches.isEmpty())
85
+        assertEquals(0, labelCounter.get())
82 86
     }
83 87
 
84 88
 }

Loading…
Cancel
Save