Browse Source

Initial work on exposing async methods

Move message processors into their own package
Update ktor
tags/v0.10.1
Chris Smith 5 years ago
parent
commit
070d502463
51 changed files with 199 additions and 58 deletions
  1. 2
    0
      CHANGELOG
  2. 2
    2
      build.gradle.kts
  3. 42
    2
      src/main/kotlin/com/dmdirc/ktirc/IrcClient.kt
  4. 17
    8
      src/main/kotlin/com/dmdirc/ktirc/IrcClientImpl.kt
  5. 2
    1
      src/main/kotlin/com/dmdirc/ktirc/events/handlers/EventHandler.kt
  6. 18
    0
      src/main/kotlin/com/dmdirc/ktirc/events/handlers/LabelledResponseHandler.kt
  7. 1
    1
      src/main/kotlin/com/dmdirc/ktirc/events/mutators/BatchMutator.kt
  8. 1
    1
      src/main/kotlin/com/dmdirc/ktirc/io/MessageHandler.kt
  9. 1
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/AccountProcessor.kt
  10. 4
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/AuthenticationProcessor.kt
  11. 1
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/BatchProcessor.kt
  12. 1
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/CapabilityProcessor.kt
  13. 1
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/ChangeHostProcessor.kt
  14. 2
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/ISupportProcessor.kt
  15. 1
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/JoinProcessor.kt
  16. 1
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/KickProcessor.kt
  17. 1
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/MessageProcessor.kt
  18. 3
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/ModeProcessor.kt
  19. 5
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/MotdProcessor.kt
  20. 1
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/NamesProcessor.kt
  21. 1
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/NickProcessor.kt
  22. 2
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/NoticeProcessor.kt
  23. 1
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/PartProcessor.kt
  24. 2
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/PingProcessor.kt
  25. 2
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/PrivmsgProcessor.kt
  26. 1
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/QuitProcessor.kt
  27. 4
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/TopicProcessor.kt
  28. 2
    1
      src/main/kotlin/com/dmdirc/ktirc/messages/processors/WelcomeProcessor.kt
  29. 25
    1
      src/main/kotlin/com/dmdirc/ktirc/model/ServerState.kt
  30. 3
    3
      src/test/kotlin/com/dmdirc/ktirc/IrcClientImplTest.kt
  31. 13
    0
      src/test/kotlin/com/dmdirc/ktirc/events/mutators/BatchMutatorTest.kt
  32. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/io/MessageHandlerTest.kt
  33. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/messages/processors/AccountProcessorTest.kt
  34. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/messages/processors/AuthenticationProcessorTest.kt
  35. 2
    1
      src/test/kotlin/com/dmdirc/ktirc/messages/processors/BatchProcessorTest.kt
  36. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/messages/processors/CapabilityProcessorTest.kt
  37. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/messages/processors/ChangeHostProcessorTest.kt
  38. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/messages/processors/ISupportProcessorTest.kt
  39. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/messages/processors/JoinProcessorTest.kt
  40. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/messages/processors/KickProcessorTest.kt
  41. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/messages/processors/ModeProcessorTest.kt
  42. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/messages/processors/MotdProcessorTest.kt
  43. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/messages/processors/NamesProcessorTest.kt
  44. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/messages/processors/NickProcessorTest.kt
  45. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/messages/processors/NoticeProcessorTest.kt
  46. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/messages/processors/PartProcessorTest.kt
  47. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/messages/processors/PrivmsgProcessorTest.kt
  48. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/messages/processors/QuitProcessorTest.kt
  49. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/messages/processors/TopicProcessorTest.kt
  50. 1
    1
      src/test/kotlin/com/dmdirc/ktirc/messages/processors/WelcomeProcessorTest.kt
  51. 16
    0
      src/test/kotlin/com/dmdirc/ktirc/model/ServerStateTest.kt

+ 2
- 0
CHANGELOG View File

@@ -1,5 +1,7 @@
1 1
 vNEXT (in development)
2 2
 
3
+ * (Internal) Moved message processors into their own package
4
+
3 5
 v0.10.0
4 6
 
5 7
  * Batch start and end events are no longer included in BatchReceived events

+ 2
- 2
build.gradle.kts View File

@@ -37,8 +37,8 @@ repositories {
37 37
 dependencies {
38 38
     implementation(kotlin("stdlib-jdk8", "1.3.21"))
39 39
     implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1")
40
-    implementation("io.ktor:ktor-network:1.1.2")
41
-    implementation("io.ktor:ktor-network-tls:1.1.2")
40
+    implementation("io.ktor:ktor-network:1.1.3")
41
+    implementation("io.ktor:ktor-network-tls:1.1.3")
42 42
 
43 43
     testImplementation("org.junit.jupiter:junit-jupiter-api:5.4.0")
44 44
     testImplementation("org.junit.jupiter:junit-jupiter-params:5.4.0")

+ 42
- 2
src/main/kotlin/com/dmdirc/ktirc/IrcClient.kt View File

@@ -5,6 +5,7 @@ import com.dmdirc.ktirc.io.CaseMapping
5 5
 import com.dmdirc.ktirc.messages.sendJoin
6 6
 import com.dmdirc.ktirc.model.*
7 7
 import com.dmdirc.ktirc.util.RemoveIn
8
+import kotlinx.coroutines.Deferred
8 9
 
9 10
 /**
10 11
  * Primary interface for interacting with KtIrc.
@@ -65,8 +66,11 @@ interface IrcClient {
65 66
      * This should only be needed to send raw/custom commands; standard messages can be sent using the
66 67
      * extension methods in [com.dmdirc.ktirc.messages] such as [sendJoin].
67 68
      *
69
+     * This method will return immediately; the message will be delivered by a coroutine. Messages
70
+     * are guaranteed to be delivered in order when this method is called multiple times.
71
+     *
68 72
      * @param tags The IRCv3 tags to prefix the message with, if any.
69
-     * @param command The command to be sent
73
+     * @param command The command to be sent.
70 74
      * @param arguments The arguments to the command.
71 75
      */
72 76
     fun send(tags: Map<MessageTag, String>, command: String, vararg arguments: String)
@@ -77,11 +81,47 @@ interface IrcClient {
77 81
      * This should only be needed to send raw/custom commands; standard messages can be sent using the
78 82
      * extension methods in [com.dmdirc.ktirc.messages] such as [sendJoin].
79 83
      *
80
-     * @param command The command to be sent
84
+     * This method will return immediately; the message will be delivered by a coroutine. Messages
85
+     * are guaranteed to be delivered in order when this method is called multiple times.
86
+     *
87
+     * @param command The command to be sent.
81 88
      * @param arguments The arguments to the command.
82 89
      */
83 90
     fun send(command: String, vararg arguments: String) = send(emptyMap(), command, *arguments)
84 91
 
92
+    /**
93
+     * Sends the given command to the IRC server, and waits for a response back.
94
+     *
95
+     * This should only be needed to send raw/custom commands; standard messages can be sent using the
96
+     * extension methods in [com.dmdirc.ktirc.messages] such as TODO: sendJoinAsync.
97
+     *
98
+     * This method will return immediately. If the server supports the labeled-responses capability,
99
+     * the returned [Deferred] will be eventually populated with the response from the server. If
100
+     * the server does not support the capability, or the response times out, `null` will be supplied.
101
+     *
102
+     * @param command The command to be sent.
103
+     * @param arguments The arguments to the command.
104
+     * @return A deferred [IrcEvent]? that contains the server's response to the command.
105
+     */
106
+    fun sendAsync(command: String, vararg arguments: String) = sendAsync(emptyMap(),  command, *arguments)
107
+
108
+    /**
109
+     * Sends the given command to the IRC server, and waits for a response back.
110
+     *
111
+     * This should only be needed to send raw/custom commands; standard messages can be sent using the
112
+     * extension methods in [com.dmdirc.ktirc.messages] such as TODO: sendJoinAsync.
113
+     *
114
+     * This method will return immediately. If the server supports the labeled-responses capability,
115
+     * the returned [Deferred] will be eventually populated with the response from the server. If
116
+     * the server does not support the capability, or the response times out, `null` will be supplied.
117
+     *
118
+     * @param tags The IRCv3 tags to prefix the message with, if any.
119
+     * @param command The command to be sent.
120
+     * @param arguments The arguments to the command.
121
+     * @return A deferred [IrcEvent]? that contains the server's response to the command.
122
+     */
123
+    fun sendAsync(tags: Map<MessageTag, String>, command: String, vararg arguments: String): Deferred<IrcEvent?>
124
+
85 125
     /**
86 126
      * Registers a new handler for all events on this connection.
87 127
      *

+ 17
- 8
src/main/kotlin/com/dmdirc/ktirc/IrcClientImpl.kt View File

@@ -8,14 +8,19 @@ 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.messages.processors.messageProcessors
11 12
 import com.dmdirc.ktirc.model.*
12 13
 import com.dmdirc.ktirc.util.currentTimeProvider
13 14
 import com.dmdirc.ktirc.util.generateLabel
14 15
 import com.dmdirc.ktirc.util.logger
15 16
 import io.ktor.util.KtorExperimentalAPI
16 17
 import kotlinx.coroutines.*
18
+import kotlinx.coroutines.channels.Channel
17 19
 import kotlinx.coroutines.channels.map
20
+import kotlinx.coroutines.time.withTimeoutOrNull
21
+import java.time.Duration
18 22
 import java.util.concurrent.atomic.AtomicBoolean
23
+import java.util.logging.Level
19 24
 
20 25
 /**
21 26
  * Concrete implementation of an [IrcClient].
@@ -33,6 +38,8 @@ internal class IrcClientImpl(private val config: IrcClientConfig) : IrcClient, C
33 38
     @KtorExperimentalAPI
34 39
     internal var socketFactory: (CoroutineScope, String, Int, Boolean) -> LineBufferedSocket = ::KtorLineBufferedSocket
35 40
 
41
+    internal var asyncTimeout = Duration.ofSeconds(20)
42
+
36 43
     override var behaviour = config.behaviour
37 44
 
38 45
     override val serverState = ServerState(config.profile.nickname, config.server.host, config.sasl)
@@ -57,16 +64,17 @@ internal class IrcClientImpl(private val config: IrcClientConfig) : IrcClient, C
57 64
                 ?: log.warning { "No send channel for command: $command" }
58 65
     }
59 66
 
60
-    // TODO: This will become sendAsync and return a Deferred<IrcEvent>
61
-    internal fun sendWithLabel(tags: Map<MessageTag, String>, command: String, vararg arguments: String) {
62
-        maybeEchoMessage(command, arguments)
63
-        val tagseToSend = if (Capability.LabeledResponse in serverState.capabilities.enabledCapabilities) {
64
-            tags + (MessageTag.Label to generateLabel(this))
67
+    override fun sendAsync(tags: Map<MessageTag, String>, command: String, vararg arguments: String) = async {
68
+        if (serverState.supportsLabeledResponses) {
69
+            val label = generateLabel(this@IrcClientImpl)
70
+            val channel = Channel<IrcEvent>(1)
71
+            serverState.labelChannels[label] = channel
72
+            send(tags + (MessageTag.Label to label), command, *arguments)
73
+            withTimeoutOrNull(asyncTimeout) { channel.receive() }.also { serverState.labelChannels.remove(label) }
65 74
         } else {
66
-            tags
75
+            send(tags, command, *arguments)
76
+            null
67 77
         }
68
-        socket?.sendChannel?.offer(messageBuilder.build(tagseToSend, command, arguments))
69
-                ?: log.warning { "No send channel for command: $command" }
70 78
     }
71 79
 
72 80
     override fun connect() {
@@ -88,6 +96,7 @@ internal class IrcClientImpl(private val config: IrcClientConfig) : IrcClient, C
88 96
                     sendUser(config.profile.username, config.profile.realName)
89 97
                     messageHandler.processMessages(this@IrcClientImpl, receiveChannel.map { parser.parse(it) })
90 98
                 } catch (ex: Exception) {
99
+                    log.log(Level.SEVERE, ex) { "Error connecting to ${config.server.host}:${config.server.port}" }
91 100
                     emitEvent(ServerConnectionError(EventMetadata(currentTimeProvider()), ex.toConnectionError(), ex.localizedMessage))
92 101
                 }
93 102
 

+ 2
- 1
src/main/kotlin/com/dmdirc/ktirc/events/handlers/EventHandler.kt View File

@@ -15,5 +15,6 @@ internal val eventHandlers = listOf(
15 15
         ChannelStateHandler(),
16 16
         PingHandler(),
17 17
         ServerStateHandler(),
18
-        UserStateHandler()
18
+        UserStateHandler(),
19
+        LabelledResponseHandler()
19 20
 )

+ 18
- 0
src/main/kotlin/com/dmdirc/ktirc/events/handlers/LabelledResponseHandler.kt View File

@@ -0,0 +1,18 @@
1
+package com.dmdirc.ktirc.events.handlers
2
+
3
+import com.dmdirc.ktirc.IrcClient
4
+import com.dmdirc.ktirc.events.IrcEvent
5
+import kotlinx.coroutines.GlobalScope
6
+import kotlinx.coroutines.launch
7
+
8
+class LabelledResponseHandler : EventHandler {
9
+
10
+    override fun processEvent(client: IrcClient, event: IrcEvent) {
11
+        event.metadata.label?.let {
12
+            GlobalScope.launch {
13
+                client.serverState.labelChannels[it]?.send(event)
14
+            }
15
+        }
16
+    }
17
+
18
+}

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

@@ -27,7 +27,7 @@ internal class BatchMutator : EventMutator {
27 27
 
28 28
     private fun finishBatch(client: IrcClient, event: BatchFinished): List<IrcEvent> {
29 29
         client.serverState.batches.remove(event.referenceId)?.let {
30
-            val batch = BatchReceived(it.events[0].metadata, it.type, it.arguments.toTypedArray(), it.events)
30
+            val batch = BatchReceived(it.metadata, it.type, it.arguments.toTypedArray(), it.events)
31 31
             if (it.metadata.batchId == null) {
32 32
                 return listOf(batch)
33 33
             } else {

+ 1
- 1
src/main/kotlin/com/dmdirc/ktirc/io/MessageHandler.kt View File

@@ -4,7 +4,7 @@ import com.dmdirc.ktirc.IrcClient
4 4
 import com.dmdirc.ktirc.events.IrcEvent
5 5
 import com.dmdirc.ktirc.events.handlers.EventHandler
6 6
 import com.dmdirc.ktirc.events.mutators.EventMutator
7
-import com.dmdirc.ktirc.messages.MessageProcessor
7
+import com.dmdirc.ktirc.messages.processors.MessageProcessor
8 8
 import com.dmdirc.ktirc.model.IrcMessage
9 9
 import com.dmdirc.ktirc.util.logger
10 10
 import kotlinx.coroutines.channels.ReceiveChannel

src/main/kotlin/com/dmdirc/ktirc/messages/AccountProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/AccountProcessor.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.UserAccountChanged
4 4
 import com.dmdirc.ktirc.model.IrcMessage

src/main/kotlin/com/dmdirc/ktirc/messages/AuthenticationProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/AuthenticationProcessor.kt View File

@@ -1,8 +1,11 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.AuthenticationMessage
4 4
 import com.dmdirc.ktirc.events.SaslFinished
5 5
 import com.dmdirc.ktirc.events.SaslMechanismNotAvailableError
6
+import com.dmdirc.ktirc.messages.ERR_SASLFAIL
7
+import com.dmdirc.ktirc.messages.RPL_SASLMECHS
8
+import com.dmdirc.ktirc.messages.RPL_SASLSUCCESS
6 9
 import com.dmdirc.ktirc.model.IrcMessage
7 10
 
8 11
 internal class AuthenticationProcessor : MessageProcessor {

src/main/kotlin/com/dmdirc/ktirc/messages/BatchProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/BatchProcessor.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.BatchFinished
4 4
 import com.dmdirc.ktirc.events.BatchStarted

src/main/kotlin/com/dmdirc/ktirc/messages/CapabilityProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/CapabilityProcessor.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.EventMetadata
4 4
 import com.dmdirc.ktirc.events.ServerCapabilitiesAcknowledged

src/main/kotlin/com/dmdirc/ktirc/messages/ChangeHostProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/ChangeHostProcessor.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.UserHostChanged
4 4
 import com.dmdirc.ktirc.model.IrcMessage

src/main/kotlin/com/dmdirc/ktirc/messages/ISupportProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/ISupportProcessor.kt View File

@@ -1,7 +1,8 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.ServerFeaturesUpdated
4 4
 import com.dmdirc.ktirc.io.CaseMapping
5
+import com.dmdirc.ktirc.messages.RPL_ISUPPORT
5 6
 import com.dmdirc.ktirc.model.IrcMessage
6 7
 import com.dmdirc.ktirc.model.ModePrefixMapping
7 8
 import com.dmdirc.ktirc.model.ServerFeatureMap

src/main/kotlin/com/dmdirc/ktirc/messages/JoinProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/JoinProcessor.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.ChannelJoined
4 4
 import com.dmdirc.ktirc.model.IrcMessage

src/main/kotlin/com/dmdirc/ktirc/messages/KickProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/KickProcessor.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.ChannelUserKicked
4 4
 import com.dmdirc.ktirc.model.IrcMessage

src/main/kotlin/com/dmdirc/ktirc/messages/MessageProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/MessageProcessor.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.IrcEvent
4 4
 import com.dmdirc.ktirc.model.IrcMessage

src/main/kotlin/com/dmdirc/ktirc/messages/ModeProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/ModeProcessor.kt View File

@@ -1,6 +1,8 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.ModeChanged
4
+import com.dmdirc.ktirc.messages.RPL_CHANNELMODEIS
5
+import com.dmdirc.ktirc.messages.RPL_UMODEIS
4 6
 import com.dmdirc.ktirc.model.IrcMessage
5 7
 
6 8
 internal class ModeProcessor : MessageProcessor {

src/main/kotlin/com/dmdirc/ktirc/messages/MotdProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/MotdProcessor.kt View File

@@ -1,7 +1,11 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.MotdFinished
4 4
 import com.dmdirc.ktirc.events.MotdLineReceived
5
+import com.dmdirc.ktirc.messages.ERR_NOMOTD
6
+import com.dmdirc.ktirc.messages.RPL_ENDOFMOTD
7
+import com.dmdirc.ktirc.messages.RPL_MOTD
8
+import com.dmdirc.ktirc.messages.RPL_MOTDSTART
5 9
 import com.dmdirc.ktirc.model.IrcMessage
6 10
 
7 11
 internal class MotdProcessor : MessageProcessor {

src/main/kotlin/com/dmdirc/ktirc/messages/NamesProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/NamesProcessor.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.ChannelNamesFinished
4 4
 import com.dmdirc.ktirc.events.ChannelNamesReceived

src/main/kotlin/com/dmdirc/ktirc/messages/NickProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/NickProcessor.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.UserNickChanged
4 4
 import com.dmdirc.ktirc.model.IrcMessage

src/main/kotlin/com/dmdirc/ktirc/messages/NoticeProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/NoticeProcessor.kt View File

@@ -1,8 +1,9 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.CtcpReplyReceived
4 4
 import com.dmdirc.ktirc.events.IrcEvent
5 5
 import com.dmdirc.ktirc.events.NoticeReceived
6
+import com.dmdirc.ktirc.messages.CTCP_BYTE
6 7
 import com.dmdirc.ktirc.model.IrcMessage
7 8
 import com.dmdirc.ktirc.model.User
8 9
 

src/main/kotlin/com/dmdirc/ktirc/messages/PartProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/PartProcessor.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.ChannelParted
4 4
 import com.dmdirc.ktirc.model.IrcMessage

src/main/kotlin/com/dmdirc/ktirc/messages/PingProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/PingProcessor.kt View File

@@ -1,6 +1,7 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.PingReceived
4
+import com.dmdirc.ktirc.messages.processors.MessageProcessor
4 5
 import com.dmdirc.ktirc.model.IrcMessage
5 6
 
6 7
 internal class PingProcessor : MessageProcessor {

src/main/kotlin/com/dmdirc/ktirc/messages/PrivmsgProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/PrivmsgProcessor.kt View File

@@ -1,9 +1,10 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.ActionReceived
4 4
 import com.dmdirc.ktirc.events.CtcpReceived
5 5
 import com.dmdirc.ktirc.events.IrcEvent
6 6
 import com.dmdirc.ktirc.events.MessageReceived
7
+import com.dmdirc.ktirc.messages.CTCP_BYTE
7 8
 import com.dmdirc.ktirc.model.IrcMessage
8 9
 import com.dmdirc.ktirc.model.User
9 10
 

src/main/kotlin/com/dmdirc/ktirc/messages/QuitProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/QuitProcessor.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.UserQuit
4 4
 import com.dmdirc.ktirc.model.IrcMessage

src/main/kotlin/com/dmdirc/ktirc/messages/TopicProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/TopicProcessor.kt View File

@@ -1,8 +1,11 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.ChannelTopicChanged
4 4
 import com.dmdirc.ktirc.events.ChannelTopicDiscovered
5 5
 import com.dmdirc.ktirc.events.ChannelTopicMetadataDiscovered
6
+import com.dmdirc.ktirc.messages.RPL_NOTOPIC
7
+import com.dmdirc.ktirc.messages.RPL_TOPIC
8
+import com.dmdirc.ktirc.messages.RPL_TOPICWHOTIME
6 9
 import com.dmdirc.ktirc.model.IrcMessage
7 10
 import com.dmdirc.ktirc.model.asUser
8 11
 import com.dmdirc.ktirc.util.currentTimeZoneProvider

src/main/kotlin/com/dmdirc/ktirc/messages/WelcomeProcessor.kt → src/main/kotlin/com/dmdirc/ktirc/messages/processors/WelcomeProcessor.kt View File

@@ -1,6 +1,7 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.ServerWelcome
4
+import com.dmdirc.ktirc.messages.RPL_WELCOME
4 5
 import com.dmdirc.ktirc.model.IrcMessage
5 6
 
6 7
 internal class WelcomeProcessor : MessageProcessor {

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

@@ -5,6 +5,7 @@ import com.dmdirc.ktirc.events.EventMetadata
5 5
 import com.dmdirc.ktirc.events.IrcEvent
6 6
 import com.dmdirc.ktirc.io.CaseMapping
7 7
 import com.dmdirc.ktirc.util.logger
8
+import kotlinx.coroutines.channels.SendChannel
8 9
 import java.util.concurrent.atomic.AtomicLong
9 10
 import kotlin.reflect.KClass
10 11
 
@@ -76,6 +77,17 @@ class ServerState internal constructor(
76 77
      */
77 78
     internal val labelCounter = AtomicLong(0)
78 79
 
80
+    /**
81
+     * Whether or not the server supports labeled responses.
82
+     */
83
+    internal val supportsLabeledResponses: Boolean
84
+        get() = Capability.LabeledResponse in capabilities.enabledCapabilities
85
+
86
+    /**
87
+     * Channels waiting for a label to be received.
88
+     */
89
+    internal val labelChannels = mutableMapOf<String, SendChannel<IrcEvent>>()
90
+
79 91
     /**
80 92
      * Determines if the given mode is one applied to a user of a channel, such as 'o' for operator.
81 93
      */
@@ -107,6 +119,7 @@ class ServerState internal constructor(
107 119
         sasl.reset()
108 120
         batches.clear()
109 121
         labelCounter.set(0)
122
+        labelChannels.clear()
110 123
     }
111 124
 
112 125
 }
@@ -122,7 +135,8 @@ class ServerFeatureMap {
122 135
      * Gets the value, or the default value, of the given feature.
123 136
      */
124 137
     @Suppress("UNCHECKED_CAST")
125
-    operator fun <T : Any> get(feature: ServerFeature<T>) = features.getOrDefault(feature, feature.default) as? T? ?: feature.default
138
+    operator fun <T : Any> get(feature: ServerFeature<T>) = features.getOrDefault(feature, feature.default) as? T?
139
+            ?: feature.default
126 140
 
127 141
     internal operator fun set(feature: ServerFeature<*>, value: Any) {
128 142
         require(feature.type.isInstance(value)) {
@@ -146,10 +160,13 @@ data class ModePrefixMapping(val modes: String, val prefixes: String) {
146 160
 
147 161
     /** Determines whether the given character is a mode prefix (e.g. "@", "+"). */
148 162
     fun isPrefix(char: Char) = prefixes.contains(char)
163
+
149 164
     /** Determines whether the given character is a channel user mode (e.g. "o", "v"). */
150 165
     fun isMode(char: Char) = modes.contains(char)
166
+
151 167
     /** Gets the mode corresponding to the given prefix (e.g. "@" -> "o"). */
152 168
     fun getMode(prefix: Char) = modes[prefixes.indexOf(prefix)]
169
+
153 170
     /** Gets the modes corresponding to the given prefixes (e.g. "@+" -> "ov"). */
154 171
     fun getModes(prefixes: String) = String(prefixes.map(this::getMode).toCharArray())
155 172
 
@@ -161,18 +178,25 @@ data class ModePrefixMapping(val modes: String, val prefixes: String) {
161 178
 sealed class ServerFeature<T : Any>(val name: String, val type: KClass<T>, val default: T? = null) {
162 179
     /** The network the server says it belongs to. */
163 180
     object Network : ServerFeature<String>("NETWORK", String::class)
181
+
164 182
     /** The case mapping the server uses, defaulting to RFC. */
165 183
     object ServerCaseMapping : ServerFeature<CaseMapping>("CASEMAPPING", CaseMapping::class, CaseMapping.Rfc)
184
+
166 185
     /** The mode prefixes the server uses, defaulting to ov/@+. */
167 186
     object ModePrefixes : ServerFeature<ModePrefixMapping>("PREFIX", ModePrefixMapping::class, ModePrefixMapping("ov", "@+"))
187
+
168 188
     /** The maximum number of channels a client may join. */
169 189
     object MaximumChannels : ServerFeature<Int>("MAXCHANNELS", Int::class) // TODO: CHANLIMIT also exists
190
+
170 191
     /** The modes supported in channels. */
171 192
     object ChannelModes : ServerFeature<Array<String>>("CHANMODES", Array<String>::class)
193
+
172 194
     /** The types of channels supported. */
173 195
     object ChannelTypes : ServerFeature<String>("CHANTYPES", String::class, "#&")
196
+
174 197
     /** The maximum length of a channel name, defaulting to 200. */
175 198
     object MaximumChannelNameLength : ServerFeature<Int>("CHANNELLEN", Int::class, 200)
199
+
176 200
     /** Whether or not the server supports extended who. */
177 201
     object WhoxSupport : ServerFeature<Boolean>("WHOX", Boolean::class, false)
178 202
 }

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

@@ -294,7 +294,7 @@ internal class IrcClientImplTest {
294 294
         client.socketFactory = mockSocketFactory
295 295
         client.connect()
296 296
 
297
-        client.sendWithLabel(tagMap(), "testing", "123")
297
+        client.sendAsync(tagMap(), "testing", "123")
298 298
 
299 299
         assertLineReceived("testing 123")
300 300
     }
@@ -307,7 +307,7 @@ internal class IrcClientImplTest {
307 307
         client.serverState.capabilities.enabledCapabilities[Capability.LabeledResponse] = ""
308 308
         client.connect()
309 309
 
310
-        client.sendWithLabel(tagMap(), "testing", "123")
310
+        client.sendAsync(tagMap(), "testing", "123")
311 311
 
312 312
         assertLineReceived("@draft/label=abc123 testing 123")
313 313
     }
@@ -320,7 +320,7 @@ internal class IrcClientImplTest {
320 320
         client.serverState.capabilities.enabledCapabilities[Capability.LabeledResponse] = ""
321 321
         client.connect()
322 322
 
323
-        client.sendWithLabel(tagMap(MessageTag.AccountName to "x"), "testing", "123")
323
+        client.sendAsync(tagMap(MessageTag.AccountName to "x"), "testing", "123")
324 324
 
325 325
         assertLineReceived("@account=x;draft/label=abc123 testing 123")
326 326
     }

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

@@ -94,6 +94,19 @@ internal class BatchMutatorTest {
94 94
         assertTrue(event.events[0] is ServerConnected)
95 95
     }
96 96
 
97
+    @Test
98
+    fun `sends finished batch with correct metadata`() {
99
+        val metadata = EventMetadata(TestConstants.time, label = "1234")
100
+        fakeServerState.batches["abcdef"] = Batch("netsplit", listOf("p1", "p2"), metadata, events = mutableListOf(ServerConnected(EventMetadata(TestConstants.time, "abcdef"))))
101
+
102
+        val events = mutator.mutateEvent(ircClient, messageEmitter, BatchFinished(EventMetadata(TestConstants.time), "abcdef"))
103
+
104
+        assertEquals(1, events.size)
105
+        assertTrue(events[0] is BatchReceived)
106
+        val event = events[0] as BatchReceived
107
+        assertSame(metadata, event.metadata)
108
+    }
109
+
97 110
     @Test
98 111
     fun `adds a batch to its parent when it finishes`() {
99 112
         fakeServerState.batches["12345"] = Batch("history", emptyList(), EventMetadata(TestConstants.time))

+ 1
- 1
src/test/kotlin/com/dmdirc/ktirc/io/MessageHandlerTest.kt View File

@@ -5,7 +5,7 @@ import com.dmdirc.ktirc.TestConstants
5 5
 import com.dmdirc.ktirc.events.*
6 6
 import com.dmdirc.ktirc.events.handlers.EventHandler
7 7
 import com.dmdirc.ktirc.events.mutators.EventMutator
8
-import com.dmdirc.ktirc.messages.MessageProcessor
8
+import com.dmdirc.ktirc.messages.processors.MessageProcessor
9 9
 import com.dmdirc.ktirc.model.IrcMessage
10 10
 import io.mockk.*
11 11
 import kotlinx.coroutines.channels.Channel

src/test/kotlin/com/dmdirc/ktirc/messages/AccountProcessorTest.kt → src/test/kotlin/com/dmdirc/ktirc/messages/processors/AccountProcessorTest.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.model.IrcMessage

src/test/kotlin/com/dmdirc/ktirc/messages/AuthenticationProcessorTest.kt → src/test/kotlin/com/dmdirc/ktirc/messages/processors/AuthenticationProcessorTest.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.events.AuthenticationMessage

src/test/kotlin/com/dmdirc/ktirc/messages/BatchProcessorTest.kt → src/test/kotlin/com/dmdirc/ktirc/messages/processors/BatchProcessorTest.kt View File

@@ -1,8 +1,9 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.events.BatchFinished
5 5
 import com.dmdirc.ktirc.events.BatchStarted
6
+import com.dmdirc.ktirc.messages.processors.BatchProcessor
6 7
 import com.dmdirc.ktirc.model.IrcMessage
7 8
 import com.dmdirc.ktirc.params
8 9
 import com.dmdirc.ktirc.util.currentTimeProvider

src/test/kotlin/com/dmdirc/ktirc/messages/CapabilityProcessorTest.kt → src/test/kotlin/com/dmdirc/ktirc/messages/processors/CapabilityProcessorTest.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.ServerCapabilitiesAcknowledged
4 4
 import com.dmdirc.ktirc.events.ServerCapabilitiesFinished

src/test/kotlin/com/dmdirc/ktirc/messages/ChangeHostProcessorTest.kt → src/test/kotlin/com/dmdirc/ktirc/messages/processors/ChangeHostProcessorTest.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.model.IrcMessage
4 4
 import com.dmdirc.ktirc.model.User

src/test/kotlin/com/dmdirc/ktirc/messages/ISupportProcessorTest.kt → src/test/kotlin/com/dmdirc/ktirc/messages/processors/ISupportProcessorTest.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.io.CaseMapping
4 4
 import com.dmdirc.ktirc.model.IrcMessage

src/test/kotlin/com/dmdirc/ktirc/messages/JoinProcessorTest.kt → src/test/kotlin/com/dmdirc/ktirc/messages/processors/JoinProcessorTest.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.model.IrcMessage

src/test/kotlin/com/dmdirc/ktirc/messages/KickProcessorTest.kt → src/test/kotlin/com/dmdirc/ktirc/messages/processors/KickProcessorTest.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.model.IrcMessage

src/test/kotlin/com/dmdirc/ktirc/messages/ModeProcessorTest.kt → src/test/kotlin/com/dmdirc/ktirc/messages/processors/ModeProcessorTest.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.model.IrcMessage

src/test/kotlin/com/dmdirc/ktirc/messages/MotdProcessorTest.kt → src/test/kotlin/com/dmdirc/ktirc/messages/processors/MotdProcessorTest.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.events.MotdFinished

src/test/kotlin/com/dmdirc/ktirc/messages/NamesProcessorTest.kt → src/test/kotlin/com/dmdirc/ktirc/messages/processors/NamesProcessorTest.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.events.ChannelNamesFinished
4 4
 import com.dmdirc.ktirc.events.ChannelNamesReceived

src/test/kotlin/com/dmdirc/ktirc/messages/NickProcessorTest.kt → src/test/kotlin/com/dmdirc/ktirc/messages/processors/NickProcessorTest.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.model.IrcMessage
4 4
 import com.dmdirc.ktirc.params

src/test/kotlin/com/dmdirc/ktirc/messages/NoticeProcessorTest.kt → src/test/kotlin/com/dmdirc/ktirc/messages/processors/NoticeProcessorTest.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.events.CtcpReplyReceived

src/test/kotlin/com/dmdirc/ktirc/messages/PartProcessorTest.kt → src/test/kotlin/com/dmdirc/ktirc/messages/processors/PartProcessorTest.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.model.IrcMessage

src/test/kotlin/com/dmdirc/ktirc/messages/PrivmsgProcessorTest.kt → src/test/kotlin/com/dmdirc/ktirc/messages/processors/PrivmsgProcessorTest.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.events.ActionReceived

src/test/kotlin/com/dmdirc/ktirc/messages/QuitProcessorTest.kt → src/test/kotlin/com/dmdirc/ktirc/messages/processors/QuitProcessorTest.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.model.IrcMessage

src/test/kotlin/com/dmdirc/ktirc/messages/TopicProcessorTest.kt → src/test/kotlin/com/dmdirc/ktirc/messages/processors/TopicProcessorTest.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.events.ChannelTopicChanged

src/test/kotlin/com/dmdirc/ktirc/messages/WelcomeProcessorTest.kt → src/test/kotlin/com/dmdirc/ktirc/messages/processors/WelcomeProcessorTest.kt View File

@@ -1,4 +1,4 @@
1
-package com.dmdirc.ktirc.messages
1
+package com.dmdirc.ktirc.messages.processors
2 2
 
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.model.IrcMessage

+ 16
- 0
src/test/kotlin/com/dmdirc/ktirc/model/ServerStateTest.kt View File

@@ -2,6 +2,7 @@ package com.dmdirc.ktirc.model
2 2
 
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.events.EventMetadata
5
+import kotlinx.coroutines.channels.Channel
5 6
 import org.junit.jupiter.api.Assertions.*
6 7
 import org.junit.jupiter.api.Test
7 8
 
@@ -60,6 +61,19 @@ internal class ServerStateTest {
60 61
         assertEquals(ChannelModeType.NoParameter, serverState.channelModeType('b'))
61 62
     }
62 63
 
64
+    @Test
65
+    fun `indicates labels are enabled when cap is present`() {
66
+        val serverState = ServerState("acidBurn", "")
67
+        serverState.capabilities.enabledCapabilities[Capability.LabeledResponse] = ""
68
+        assertTrue(serverState.supportsLabeledResponses)
69
+    }
70
+
71
+    @Test
72
+    fun `indicates labels are not enabled when cap is absent`() {
73
+        val serverState = ServerState("acidBurn", "")
74
+        assertFalse(serverState.supportsLabeledResponses)
75
+    }
76
+
63 77
     @Test
64 78
     fun `reset clears all state`() = with(ServerState("acidBurn", "")) {
65 79
         receivedWelcome = true
@@ -71,6 +85,7 @@ internal class ServerStateTest {
71 85
         sasl.saslBuffer = "in progress"
72 86
         batches["batch"] = Batch("type", emptyList(), EventMetadata(TestConstants.time))
73 87
         labelCounter.set(100)
88
+        labelChannels["#thegibson"] = Channel(1)
74 89
 
75 90
         reset()
76 91
 
@@ -83,6 +98,7 @@ internal class ServerStateTest {
83 98
         assertEquals("", sasl.saslBuffer)
84 99
         assertTrue(batches.isEmpty())
85 100
         assertEquals(0, labelCounter.get())
101
+        assertTrue(labelChannels.isEmpty())
86 102
     }
87 103
 
88 104
 }

Loading…
Cancel
Save