Browse Source

Raise ServerConnected event, track status nicely

tags/v0.3.0
Chris Smith 5 years ago
parent
commit
c6b0471761

+ 3
- 0
CHANGELOG View File

@@ -4,6 +4,9 @@ vNEXT (in development)
4 4
     Instead of: client.send(joinMessage("#channel"))
5 5
     Now use: client.sendJoin("#channel")
6 6
   * Added reply utility to easily send replies to message events
7
+  * Server state improvements:
8
+    * Added status field to ServerState
9
+    * ServerConnected event is emitted as soon as the socket is connected
7 10
 
8 11
 v0.2.1
9 12
 

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

@@ -4,6 +4,7 @@ import com.dmdirc.ktirc.events.*
4 4
 import com.dmdirc.ktirc.io.*
5 5
 import com.dmdirc.ktirc.messages.*
6 6
 import com.dmdirc.ktirc.model.*
7
+import com.dmdirc.ktirc.util.currentTimeProvider
7 8
 import kotlinx.coroutines.*
8 9
 import kotlinx.coroutines.channels.map
9 10
 import java.util.concurrent.atomic.AtomicBoolean
@@ -108,14 +109,15 @@ class IrcClientImpl(private val server: Server, private val profile: Profile) :
108 109
         check(!connecting.getAndSet(true))
109 110
         connectionJob = scope.launch {
110 111
             with(socketFactory(server.host, server.port, server.tls)) {
112
+                // TODO: Proper error handling - what if connect() fails?
111 113
                 socket = this
112 114
                 connect()
113
-                sendLine("CAP LS 302")
115
+                messageHandler.emitEvent(this@IrcClientImpl, ServerConnected(currentTimeProvider()))
116
+                sendLine("CAP LS 302") // TODO: Stick this in a builder
114 117
                 server.password?.let { pass -> sendPassword(pass) }
115 118
                 sendNickChange(profile.initialNick)
116 119
                 // TODO: Send correct host
117 120
                 sendUser(profile.userName, "localhost", server.host, profile.realName)
118
-                // TODO: This should be elsewhere
119 121
                 messageHandler.processMessages(this@IrcClientImpl, readLines(scope).map { parser.parse(it) })
120 122
             }
121 123
         }

+ 3
- 4
src/main/kotlin/com/dmdirc/ktirc/events/Events.kt View File

@@ -8,16 +8,15 @@ import java.time.LocalDateTime
8 8
 /** Base class for all events. */
9 9
 sealed class IrcEvent(val time: LocalDateTime)
10 10
 
11
+/** Raised when the connection to the server has been established. The server will not be ready for use yet. */
12
+class ServerConnected(time: LocalDateTime) : IrcEvent(time)
13
+
11 14
 /** Raised when the server initially welcomes us to the IRC network. */
12 15
 class ServerWelcome(time: LocalDateTime, val localNick: String) : IrcEvent(time)
13 16
 
14 17
 /** Raised when the features supported by the server have changed. This may occur numerous times. */
15 18
 class ServerFeaturesUpdated(time: LocalDateTime, val serverFeatures: ServerFeatureMap) : IrcEvent(time)
16 19
 
17
-/** Raised when the connection to the server has been established, configuration information has been received, etc. */
18
-// TODO: Implement
19
-class ServerConnected(time: LocalDateTime) : IrcEvent(time)
20
-
21 20
 /** Raised whenever a PING is received from the server. */
22 21
 class PingReceived(time: LocalDateTime, val nonce: ByteArray) : IrcEvent(time)
23 22
 

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

@@ -2,27 +2,29 @@ package com.dmdirc.ktirc.io
2 2
 
3 3
 import com.dmdirc.ktirc.IrcClient
4 4
 import com.dmdirc.ktirc.events.EventHandler
5
+import com.dmdirc.ktirc.events.IrcEvent
5 6
 import com.dmdirc.ktirc.messages.MessageProcessor
6 7
 import com.dmdirc.ktirc.model.IrcMessage
7 8
 import com.dmdirc.ktirc.util.logger
8 9
 import kotlinx.coroutines.channels.ReceiveChannel
9
-import kotlinx.coroutines.channels.consumeEach
10 10
 
11 11
 internal class MessageHandler(private val processors: List<MessageProcessor>, val handlers: MutableList<EventHandler>) {
12 12
 
13 13
     private val log by logger()
14 14
 
15 15
     suspend fun processMessages(ircClient: IrcClient, messages: ReceiveChannel<IrcMessage>) {
16
-        messages.consumeEach {
17
-            it.process().forEach { event ->
18
-                handlers.forEach { handler ->
19
-                    handler.processEvent(ircClient, event)
20
-                }
21
-            }
16
+        for (message in messages) {
17
+            message.toEvents().forEach { event -> emitEvent(ircClient, event) }
22 18
         }
23 19
     }
24 20
 
25
-    private fun IrcMessage.process() = this.getProcessor()?.process(this) ?: emptyList()
21
+    fun emitEvent(ircClient: IrcClient, ircEvent: IrcEvent) {
22
+        handlers.forEach { handler ->
23
+            handler.processEvent(ircClient, ircEvent)
24
+        }
25
+    }
26
+
27
+    private fun IrcMessage.toEvents() = this.getProcessor()?.process(this) ?: emptyList()
26 28
     private fun IrcMessage.getProcessor() = processors.firstOrNull { it.commands.contains(command) } ?: run {
27 29
         log.warning { "No processor found for $command" }
28 30
         null

+ 2
- 6
src/main/kotlin/com/dmdirc/ktirc/model/IrcMessage.kt View File

@@ -1,16 +1,12 @@
1 1
 package com.dmdirc.ktirc.model
2 2
 
3
+import com.dmdirc.ktirc.util.currentTimeProvider
4
+import com.dmdirc.ktirc.util.currentTimeZoneProvider
3 5
 import java.time.Instant
4 6
 import java.time.LocalDateTime
5
-import java.time.ZoneId
6 7
 
7 8
 class IrcMessage(val tags: Map<MessageTag, String>, val prefix: ByteArray?, val command: String, val params: List<ByteArray>) {
8 9
 
9
-    companion object {
10
-        internal var currentTimeZoneProvider = { ZoneId.systemDefault() }
11
-        internal var currentTimeProvider = { LocalDateTime.now(currentTimeZoneProvider()) }
12
-    }
13
-
14 10
     val time: LocalDateTime = if (MessageTag.ServerTime in tags) {
15 11
         LocalDateTime.ofInstant(Instant.parse(tags[MessageTag.ServerTime]), currentTimeZoneProvider())
16 12
     } else {

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

@@ -3,10 +3,23 @@ package com.dmdirc.ktirc.model
3 3
 import com.dmdirc.ktirc.io.CaseMapping
4 4
 import kotlin.reflect.KClass
5 5
 
6
-class ServerState(initialNickname: String) {
6
+/**
7
+ * Contains the current state of a single IRC server.
8
+ */
9
+class ServerState internal constructor(initialNickname: String) {
7 10
 
11
+    /** The current status of the server. */
12
+    var status = ServerStatus.Connecting
13
+        internal set
14
+
15
+    /** Our present nickname on the server. */
8 16
     var localNickname: String = initialNickname
17
+        internal set
18
+
19
+    /** The features that the server has declared it supports (from the 005 header). */
9 20
     val features = ServerFeatureMap()
21
+
22
+    /** The capabilities we have negotiated with the server (from IRCv3). */
10 23
     val capabilities = CapabilitiesState()
11 24
 
12 25
 }
@@ -48,3 +61,15 @@ sealed class ServerFeature<T : Any>(val name: String, val type: KClass<T>, val d
48 61
 internal val serverFeatures: Map<String, ServerFeature<*>> by lazy {
49 62
     ServerFeature::class.nestedClasses.map { it.objectInstance as ServerFeature<*> }.associateBy { it.name }
50 63
 }
64
+
65
+/**
66
+ * Enumeration of the possible states of a server.
67
+ */
68
+enum class ServerStatus {
69
+    /** We are attempting to connect to the server. It is not yet ready for use. */
70
+    Connecting,
71
+    /** We are logging in, dealing with capabilities, etc. The server is not yet ready for use. */
72
+    Negotiating,
73
+    /** We are connected and commands can be sent. */
74
+    Ready,
75
+}

+ 7
- 0
src/main/kotlin/com/dmdirc/ktirc/util/Time.kt View File

@@ -0,0 +1,7 @@
1
+package com.dmdirc.ktirc.util
2
+
3
+import java.time.LocalDateTime
4
+import java.time.ZoneId
5
+
6
+internal var currentTimeZoneProvider = { ZoneId.systemDefault() }
7
+internal var currentTimeProvider = { LocalDateTime.now(currentTimeZoneProvider()) }

+ 20
- 2
src/test/kotlin/com/dmdirc/ktirc/IrcClientTest.kt View File

@@ -1,10 +1,15 @@
1 1
 package com.dmdirc.ktirc
2 2
 
3 3
 import com.dmdirc.ktirc.events.IrcEvent
4
+import com.dmdirc.ktirc.events.ServerConnected
4 5
 import com.dmdirc.ktirc.events.ServerWelcome
5 6
 import com.dmdirc.ktirc.io.CaseMapping
6 7
 import com.dmdirc.ktirc.io.LineBufferedSocket
7
-import com.dmdirc.ktirc.model.*
8
+import com.dmdirc.ktirc.model.Profile
9
+import com.dmdirc.ktirc.model.Server
10
+import com.dmdirc.ktirc.model.ServerFeature
11
+import com.dmdirc.ktirc.model.User
12
+import com.dmdirc.ktirc.util.currentTimeProvider
8 13
 import com.nhaarman.mockitokotlin2.*
9 14
 import kotlinx.coroutines.GlobalScope
10 15
 import kotlinx.coroutines.channels.Channel
@@ -41,7 +46,7 @@ internal class IrcClientImplTest {
41 46
 
42 47
     @BeforeEach
43 48
     fun setUp() {
44
-        IrcMessage.currentTimeProvider = { TestConstants.time }
49
+        currentTimeProvider = { TestConstants.time }
45 50
     }
46 51
 
47 52
     @Test
@@ -73,6 +78,19 @@ internal class IrcClientImplTest {
73 78
         }
74 79
     }
75 80
 
81
+    @Test
82
+    fun `IrcClientImpl emits connected event with local time`() = runBlocking {
83
+        currentTimeProvider = { TestConstants.time }
84
+        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
85
+        client.socketFactory = mockSocketFactory
86
+        client.onEvent(mockEventHandler)
87
+        client.connect()
88
+
89
+        val captor = argumentCaptor<ServerConnected>()
90
+        verify(mockEventHandler, timeout(500)).invoke(captor.capture())
91
+        assertEquals(TestConstants.time, captor.firstValue.time)
92
+    }
93
+
76 94
     @Test
77 95
     fun `IrcClientImpl sends basic connection strings`() = runBlocking {
78 96
         val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))

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

@@ -25,7 +25,7 @@ internal class MessageHandlerTest {
25 25
     }
26 26
 
27 27
     @Test
28
-    fun `MessageHandler passes message on to correct processor`() = runBlocking {
28
+    fun `MessageHandler passes message on to correct processor`() = runBlocking<Unit> {
29 29
         val handler = MessageHandler(listOf(joinProcessor, nickProcessor), mutableListOf())
30 30
         val message = IrcMessage(emptyMap(), null, "JOIN", emptyList())
31 31
 
@@ -36,11 +36,10 @@ internal class MessageHandlerTest {
36 36
         }
37 37
 
38 38
         verify(joinProcessor).process(message)
39
-        Unit
40 39
     }
41 40
 
42 41
     @Test
43
-    fun `MessageHandler reads multiple messages`() = runBlocking {
42
+    fun `MessageHandler reads multiple messages`() = runBlocking<Unit> {
44 43
         val handler = MessageHandler(listOf(joinProcessor, nickProcessor), mutableListOf())
45 44
         val joinMessage = IrcMessage(emptyMap(), null, "JOIN", emptyList())
46 45
         val nickMessage = IrcMessage(emptyMap(), null, "NICK", emptyList())
@@ -58,7 +57,6 @@ internal class MessageHandlerTest {
58 57
             verify(joinProcessor).process(joinMessage)
59 58
             verify(nickProcessor).process(nickMessage)
60 59
         }
61
-        Unit
62 60
     }
63 61
 
64 62
     @Test
@@ -81,4 +79,15 @@ internal class MessageHandlerTest {
81 79
         verify(eventHandler2).processEvent(same(ircClient), isA<ServerWelcome>())
82 80
     }
83 81
 
82
+    @Test
83
+    fun `MessageHandler emits custom events to all handlers`() = runBlocking {
84
+        val eventHandler1 = mock<EventHandler>()
85
+        val eventHandler2 = mock<EventHandler>()
86
+        val handler = MessageHandler(emptyList(), mutableListOf(eventHandler1, eventHandler2))
87
+        handler.emitEvent(ircClient, ServerWelcome(TestConstants.time, "abc"))
88
+
89
+        verify(eventHandler1).processEvent(same(ircClient), isA<ServerWelcome>())
90
+        verify(eventHandler2).processEvent(same(ircClient), isA<ServerWelcome>())
91
+    }
92
+
84 93
 }

+ 2
- 1
src/test/kotlin/com/dmdirc/ktirc/messages/JoinProcessorTest.kt View File

@@ -3,6 +3,7 @@ package com.dmdirc.ktirc.messages
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.model.IrcMessage
5 5
 import com.dmdirc.ktirc.model.User
6
+import com.dmdirc.ktirc.util.currentTimeProvider
6 7
 import org.junit.jupiter.api.Assertions.assertEquals
7 8
 import org.junit.jupiter.api.BeforeEach
8 9
 import org.junit.jupiter.api.Test
@@ -11,7 +12,7 @@ internal class JoinProcessorTest {
11 12
 
12 13
     @BeforeEach
13 14
     fun setUp() {
14
-        IrcMessage.currentTimeProvider = { TestConstants.time }
15
+        currentTimeProvider = { TestConstants.time }
15 16
     }
16 17
 
17 18
     @Test

+ 2
- 1
src/test/kotlin/com/dmdirc/ktirc/messages/PartProcessorTest.kt View File

@@ -3,6 +3,7 @@ package com.dmdirc.ktirc.messages
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.model.IrcMessage
5 5
 import com.dmdirc.ktirc.model.User
6
+import com.dmdirc.ktirc.util.currentTimeProvider
6 7
 import org.junit.jupiter.api.Assertions.assertEquals
7 8
 import org.junit.jupiter.api.BeforeEach
8 9
 import org.junit.jupiter.api.Test
@@ -11,7 +12,7 @@ internal class PartProcessorTest {
11 12
 
12 13
     @BeforeEach
13 14
     fun setUp() {
14
-        IrcMessage.currentTimeProvider = { TestConstants.time }
15
+        currentTimeProvider = { TestConstants.time }
15 16
     }
16 17
 
17 18
     @Test

+ 2
- 1
src/test/kotlin/com/dmdirc/ktirc/messages/PrivmsgProcessorTest.kt View File

@@ -3,6 +3,7 @@ package com.dmdirc.ktirc.messages
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.model.IrcMessage
5 5
 import com.dmdirc.ktirc.model.User
6
+import com.dmdirc.ktirc.util.currentTimeProvider
6 7
 import org.junit.jupiter.api.Assertions.assertEquals
7 8
 import org.junit.jupiter.api.BeforeEach
8 9
 import org.junit.jupiter.api.Test
@@ -11,7 +12,7 @@ internal class PrivmsgProcessorTest {
11 12
 
12 13
     @BeforeEach
13 14
     fun setUp() {
14
-        IrcMessage.currentTimeProvider = { TestConstants.time }
15
+        currentTimeProvider = { TestConstants.time }
15 16
     }
16 17
 
17 18
     @Test

+ 2
- 1
src/test/kotlin/com/dmdirc/ktirc/messages/QuitProcessorTest.kt View File

@@ -3,6 +3,7 @@ package com.dmdirc.ktirc.messages
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.model.IrcMessage
5 5
 import com.dmdirc.ktirc.model.User
6
+import com.dmdirc.ktirc.util.currentTimeProvider
6 7
 import org.junit.jupiter.api.Assertions.assertEquals
7 8
 import org.junit.jupiter.api.BeforeEach
8 9
 import org.junit.jupiter.api.Test
@@ -11,7 +12,7 @@ internal class QuitProcessorTest {
11 12
 
12 13
     @BeforeEach
13 14
     fun setUp() {
14
-        IrcMessage.currentTimeProvider = { TestConstants.time }
15
+        currentTimeProvider = { TestConstants.time }
15 16
     }
16 17
 
17 18
     @Test

+ 2
- 1
src/test/kotlin/com/dmdirc/ktirc/messages/WelcomeProcessorTest.kt View File

@@ -2,6 +2,7 @@ package com.dmdirc.ktirc.messages
2 2
 
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.model.IrcMessage
5
+import com.dmdirc.ktirc.util.currentTimeProvider
5 6
 import org.junit.jupiter.api.Assertions.assertEquals
6 7
 import org.junit.jupiter.api.Assertions.assertTrue
7 8
 import org.junit.jupiter.api.BeforeEach
@@ -13,7 +14,7 @@ internal class WelcomeProcessorTest {
13 14
 
14 15
     @BeforeEach
15 16
     fun setUp() {
16
-        IrcMessage.currentTimeProvider = { TestConstants.time }
17
+        currentTimeProvider = { TestConstants.time }
17 18
     }
18 19
 
19 20
     @Test

+ 5
- 3
src/test/kotlin/com/dmdirc/ktirc/model/IrcMessageTest.kt View File

@@ -1,6 +1,8 @@
1 1
 package com.dmdirc.ktirc.model
2 2
 
3 3
 import com.dmdirc.ktirc.TestConstants
4
+import com.dmdirc.ktirc.util.currentTimeProvider
5
+import com.dmdirc.ktirc.util.currentTimeZoneProvider
4 6
 import org.junit.jupiter.api.Assertions.assertEquals
5 7
 import org.junit.jupiter.api.Assertions.assertNull
6 8
 import org.junit.jupiter.api.Test
@@ -11,21 +13,21 @@ internal class IrcMessageTest {
11 13
 
12 14
     @Test
13 15
     fun `Gets UTC time from ServerTime tag if present`() {
14
-        IrcMessage.currentTimeZoneProvider = { ZoneId.of("Z") }
16
+        currentTimeZoneProvider = { ZoneId.of("Z") }
15 17
         val message = IrcMessage(hashMapOf(MessageTag.ServerTime to "1995-09-15T09:00:00.0000Z"), null, "", emptyList())
16 18
         assertEquals(LocalDateTime.parse("1995-09-15T09:00:00"), message.time)
17 19
     }
18 20
 
19 21
     @Test
20 22
     fun `Converts time in ServerTime tag to local timezone`() {
21
-        IrcMessage.currentTimeZoneProvider = { ZoneId.of("America/New_York") }
23
+        currentTimeZoneProvider = { ZoneId.of("America/New_York") }
22 24
         val message = IrcMessage(hashMapOf(MessageTag.ServerTime to "1995-09-15T09:00:00.0000Z"), null, "", emptyList())
23 25
         assertEquals(LocalDateTime.parse("1995-09-15T05:00:00"), message.time)
24 26
     }
25 27
 
26 28
     @Test
27 29
     fun `Uses current local time if no tag present`() {
28
-        IrcMessage.currentTimeProvider = { TestConstants.time }
30
+        currentTimeProvider = { TestConstants.time }
29 31
         val message = IrcMessage(emptyMap(), null, "", emptyList())
30 32
         assertEquals(LocalDateTime.parse("1995-09-15T09:00:00"), message.time)
31 33
     }

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

@@ -6,11 +6,17 @@ import org.junit.jupiter.api.Test
6 6
 internal class ServerStateTest {
7 7
 
8 8
     @Test
9
-    fun `IrcServerState should use the initial nickname as local nickname`() {
9
+    fun `ServerState should use the initial nickname as local nickname`() {
10 10
         val serverState = ServerState("acidBurn")
11 11
         assertEquals("acidBurn", serverState.localNickname)
12 12
     }
13 13
 
14
+    @Test
15
+    fun `ServerState should default status to connecting`() {
16
+        val serverState = ServerState("acidBurn")
17
+        assertEquals(ServerStatus.Connecting, serverState.status)
18
+    }
19
+
14 20
 }
15 21
 
16 22
 internal class ModePrefixMappingTest {

Loading…
Cancel
Save