Browse Source

Add channel join handling, basic channel state

tags/v0.1.0
Chris Smith 5 years ago
parent
commit
3cb6a28323

+ 9
- 7
src/main/kotlin/com/dmdirc/ktirc/IrcClient.kt View File

@@ -4,14 +4,9 @@ import com.dmdirc.ktirc.events.EventHandler
4 4
 import com.dmdirc.ktirc.events.IrcEvent
5 5
 import com.dmdirc.ktirc.events.ServerWelcome
6 6
 import com.dmdirc.ktirc.events.eventHandlers
7
-import com.dmdirc.ktirc.io.KtorLineBufferedSocket
8
-import com.dmdirc.ktirc.io.LineBufferedSocket
9
-import com.dmdirc.ktirc.io.MessageHandler
10
-import com.dmdirc.ktirc.io.MessageParser
7
+import com.dmdirc.ktirc.io.*
11 8
 import com.dmdirc.ktirc.messages.*
12
-import com.dmdirc.ktirc.model.Profile
13
-import com.dmdirc.ktirc.model.Server
14
-import com.dmdirc.ktirc.model.ServerState
9
+import com.dmdirc.ktirc.model.*
15 10
 import kotlinx.coroutines.channels.map
16 11
 import kotlinx.coroutines.coroutineScope
17 12
 import kotlinx.coroutines.runBlocking
@@ -24,6 +19,12 @@ interface IrcClient {
24 19
     suspend fun send(message: String)
25 20
 
26 21
     val serverState: ServerState
22
+    val channelState: ChannelStateMap
23
+
24
+    val caseMapping: CaseMapping
25
+        get() = serverState.features[ServerFeature.ServerCaseMapping] ?: CaseMapping.Rfc
26
+
27
+    fun isLocalUser(user: User): Boolean = caseMapping.areEquivalent(user.nickname, serverState.localNickname)
27 28
 
28 29
 }
29 30
 
@@ -35,6 +36,7 @@ class IrcClientImpl(private val server: Server, private val profile: Profile) :
35 36
     var socketFactory: (String, Int) -> LineBufferedSocket = ::KtorLineBufferedSocket
36 37
 
37 38
     override val serverState = ServerState(profile.initialNick)
39
+    override val channelState = ChannelStateMap { caseMapping }
38 40
 
39 41
     private val messageHandler = MessageHandler(messageProcessors, eventHandlers + object : EventHandler {
40 42
         override suspend fun processEvent(client: IrcClient, event: IrcEvent) {

+ 25
- 0
src/main/kotlin/com/dmdirc/ktirc/events/ChannelStateHandler.kt View File

@@ -0,0 +1,25 @@
1
+package com.dmdirc.ktirc.events
2
+
3
+import com.dmdirc.ktirc.IrcClient
4
+import com.dmdirc.ktirc.model.ChannelState
5
+import com.dmdirc.ktirc.util.logger
6
+
7
+class ChannelStateHandler : EventHandler {
8
+
9
+    private val log by logger()
10
+
11
+    override suspend fun processEvent(client: IrcClient, event: IrcEvent) {
12
+        when (event) {
13
+            is ChannelJoined -> handleJoin(client, event)
14
+        }
15
+    }
16
+
17
+    private fun handleJoin(client: IrcClient, event: ChannelJoined) {
18
+        if (client.isLocalUser(event.user)) {
19
+            log.info { "Joined new channel: ${event.channel}" }
20
+            client.channelState += ChannelState(event.channel)
21
+        }
22
+        // TODO: Add user to channel
23
+    }
24
+
25
+}

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

@@ -3,6 +3,7 @@
3 3
 package com.dmdirc.ktirc.events
4 4
 
5 5
 import com.dmdirc.ktirc.model.ServerFeatureMap
6
+import com.dmdirc.ktirc.model.User
6 7
 
7 8
 sealed class IrcEvent
8 9
 
@@ -25,4 +26,9 @@ object ServerConnected : IrcEvent()
25 26
 /**
26 27
  * Raised whenever a PING is received from the server.
27 28
  */
28
-data class PingReceived(val nonce: ByteArray): IrcEvent()
29
+data class PingReceived(val nonce: ByteArray): IrcEvent()
30
+
31
+/**
32
+ * Raised when a user joins a channel.
33
+ */
34
+data class ChannelJoined(val user: User, val channel: String): IrcEvent()

+ 13
- 0
src/main/kotlin/com/dmdirc/ktirc/messages/JoinProcessor.kt View File

@@ -0,0 +1,13 @@
1
+package com.dmdirc.ktirc.messages
2
+
3
+import com.dmdirc.ktirc.events.ChannelJoined
4
+import com.dmdirc.ktirc.io.IrcMessage
5
+import com.dmdirc.ktirc.model.asUser
6
+
7
+class JoinProcessor : MessageProcessor {
8
+
9
+    override val commands = arrayOf("JOIN")
10
+
11
+    override fun process(message: IrcMessage) = message.prefix?.let { listOf(ChannelJoined(it.asUser(), String(message.params[0]))) } ?: emptyList()
12
+
13
+}

+ 1
- 0
src/main/kotlin/com/dmdirc/ktirc/messages/MessageProcessor.kt View File

@@ -19,6 +19,7 @@ interface MessageProcessor {
19 19
 
20 20
 val messageProcessors = setOf(
21 21
         ISupportProcessor(),
22
+        JoinProcessor(),
22 23
         PingProcessor(),
23 24
         WelcomeProcessor()
24 25
 )

+ 3
- 0
src/main/kotlin/com/dmdirc/ktirc/model/ChannelState.kt View File

@@ -0,0 +1,3 @@
1
+package com.dmdirc.ktirc.model
2
+
3
+class ChannelState(val name: String)

+ 24
- 0
src/main/kotlin/com/dmdirc/ktirc/model/ChannelStateMap.kt View File

@@ -0,0 +1,24 @@
1
+package com.dmdirc.ktirc.model
2
+
3
+import com.dmdirc.ktirc.io.CaseMapping
4
+
5
+class ChannelStateMap(private val caseMappingProvider: () -> CaseMapping) : Iterable<ChannelState> {
6
+
7
+    private val channels = HashSet<ChannelState>()
8
+
9
+    operator fun get(name: String) = channels.find { caseMappingProvider().areEquivalent(it.name, name) }
10
+
11
+    operator fun plusAssign(state: ChannelState) {
12
+        require(get(state.name) == null) { "Channel state already registered: ${state.name}"}
13
+        channels.add(state)
14
+    }
15
+
16
+    operator fun minusAssign(state: ChannelState) {
17
+        channels.removeIf { caseMappingProvider().areEquivalent(it.name, state.name) }
18
+    }
19
+
20
+    operator fun contains(name: String) = get(name) != null
21
+
22
+    override fun iterator() = channels.iterator()
23
+
24
+}

+ 18
- 0
src/main/kotlin/com/dmdirc/ktirc/model/User.kt View File

@@ -0,0 +1,18 @@
1
+package com.dmdirc.ktirc.model
2
+
3
+data class User(val nickname: String, val ident: String? = null, val hostname: String? = null)
4
+
5
+fun ByteArray.asUser(): User {
6
+    val string = String(this)
7
+    val identOffset = string.indexOf('!')
8
+    return if (identOffset >= 0) {
9
+        val hostOffset = string.indexOf('@', identOffset)
10
+        if (hostOffset >= 0) {
11
+            User(string.substring(0 until identOffset), string.substring(identOffset + 1 until hostOffset), string.substring(hostOffset + 1))
12
+        } else {
13
+            User(string.substring(0 until identOffset), string.substring(identOffset + 1))
14
+        }
15
+    } else {
16
+        User(string)
17
+    }
18
+}

src/test/kotlin/com/dmdirc/ktirc/IrcClientImplTest.kt → src/test/kotlin/com/dmdirc/ktirc/IrcClientTest.kt View File

@@ -1,11 +1,15 @@
1 1
 package com.dmdirc.ktirc
2 2
 
3
+import com.dmdirc.ktirc.io.CaseMapping
3 4
 import com.dmdirc.ktirc.io.LineBufferedSocket
4 5
 import com.dmdirc.ktirc.model.Profile
5 6
 import com.dmdirc.ktirc.model.Server
7
+import com.dmdirc.ktirc.model.ServerFeature
8
+import com.dmdirc.ktirc.model.User
6 9
 import com.nhaarman.mockitokotlin2.*
7 10
 import kotlinx.coroutines.channels.Channel
8 11
 import kotlinx.coroutines.runBlocking
12
+import org.junit.jupiter.api.Assertions.*
9 13
 import org.junit.jupiter.api.Test
10 14
 import org.junit.jupiter.api.assertThrows
11 15
 
@@ -92,4 +96,28 @@ internal class IrcClientImplTest {
92 96
         }
93 97
     }
94 98
 
99
+    @Test
100
+    fun `IrcClient gets case mapping from server features`() {
101
+        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
102
+        client.serverState.features[ServerFeature.ServerCaseMapping] = CaseMapping.RfcStrict
103
+        assertEquals(CaseMapping.RfcStrict, client.caseMapping)
104
+    }
105
+
106
+    @Test
107
+    fun `IrcClient indicates if user is local user or not`() {
108
+        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
109
+        client.serverState.localNickname = "[acidBurn]"
110
+
111
+        assertTrue(client.isLocalUser(User("{acidBurn}", "libby", "root.localhost")))
112
+        assertFalse(client.isLocalUser(User("acid-Burn", "libby", "root.localhost")))
113
+    }
114
+
115
+    @Test
116
+    fun `IrcClient uses current case mapping to check local user`() {
117
+        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
118
+        client.serverState.localNickname = "[acidBurn]"
119
+        client.serverState.features[ServerFeature.ServerCaseMapping] = CaseMapping.Ascii
120
+        assertFalse(client.isLocalUser(User("{acidBurn}", "libby", "root.localhost")))
121
+    }
122
+
95 123
 }

+ 36
- 0
src/test/kotlin/com/dmdirc/ktirc/events/ChannelStateHandlerTest.kt View File

@@ -0,0 +1,36 @@
1
+package com.dmdirc.ktirc.events
2
+
3
+import com.dmdirc.ktirc.IrcClient
4
+import com.dmdirc.ktirc.io.CaseMapping
5
+import com.dmdirc.ktirc.model.ChannelStateMap
6
+import com.dmdirc.ktirc.model.User
7
+import com.nhaarman.mockitokotlin2.doReturn
8
+import com.nhaarman.mockitokotlin2.mock
9
+import kotlinx.coroutines.runBlocking
10
+import org.junit.jupiter.api.Assertions.assertFalse
11
+import org.junit.jupiter.api.Assertions.assertTrue
12
+import org.junit.jupiter.api.Test
13
+
14
+internal class ChannelStateHandlerTest {
15
+
16
+    private val channelStateMap = ChannelStateMap { CaseMapping.Rfc }
17
+    private val ircClient = mock<IrcClient> {
18
+        on { channelState } doReturn channelStateMap
19
+        on { isLocalUser(User("acidburn", "libby", "root.localhost")) } doReturn true
20
+    }
21
+
22
+    @Test
23
+    fun `ChannelStateHandler creates new state object for local joins`() = runBlocking {
24
+        val handler = ChannelStateHandler()
25
+        handler.processEvent(ircClient, ChannelJoined(User("acidburn", "libby", "root.localhost"), "#newchannel"))
26
+        assertTrue("#newchannel" in channelStateMap)
27
+    }
28
+
29
+    @Test
30
+    fun `ChannelStateHandler does not create new state object for remote joins`() = runBlocking {
31
+        val handler = ChannelStateHandler()
32
+        handler.processEvent(ircClient, ChannelJoined(User("zerocool", "dade", "root.localhost"), "#newchannel"))
33
+        assertFalse("#newchannel" in channelStateMap)
34
+    }
35
+
36
+}

+ 26
- 0
src/test/kotlin/com/dmdirc/ktirc/messages/JoinProcessorTest.kt View File

@@ -0,0 +1,26 @@
1
+package com.dmdirc.ktirc.messages
2
+
3
+import com.dmdirc.ktirc.events.ChannelJoined
4
+import com.dmdirc.ktirc.io.IrcMessage
5
+import com.dmdirc.ktirc.model.User
6
+import org.junit.jupiter.api.Assertions.assertEquals
7
+import org.junit.jupiter.api.Test
8
+
9
+internal class JoinProcessorTest {
10
+
11
+    @Test
12
+    fun `JoinProcessor raises join event`() {
13
+        val events = JoinProcessor().process(
14
+                IrcMessage(null, "acidburn!libby@root.localhost".toByteArray(), "JOIN", listOf("#crashandburn".toByteArray())))
15
+        assertEquals(1, events.size)
16
+        assertEquals(ChannelJoined(User("acidburn", "libby", "root.localhost"), "#crashandburn"), events[0])
17
+    }
18
+
19
+    @Test
20
+    fun `JoinProcessor does nothing if prefix missing`() {
21
+        val events = JoinProcessor().process(
22
+                IrcMessage(null, null, "JOIN", listOf("#crashandburn".toByteArray())))
23
+        assertEquals(0, events.size)
24
+    }
25
+
26
+}

+ 78
- 0
src/test/kotlin/com/dmdirc/ktirc/model/ChannelStateMapTest.kt View File

@@ -0,0 +1,78 @@
1
+package com.dmdirc.ktirc.model
2
+
3
+import com.dmdirc.ktirc.io.CaseMapping
4
+import org.junit.jupiter.api.Assertions.*
5
+import org.junit.jupiter.api.Test
6
+import org.junit.jupiter.api.assertThrows
7
+
8
+internal class ChannelStateMapTest {
9
+
10
+    @Test
11
+    fun `ChannelStateMap stores channel state`() {
12
+        val channelStateMap = ChannelStateMap { CaseMapping.Rfc }
13
+        val channelState = ChannelState("#dumpsterdiving")
14
+        channelStateMap += channelState
15
+
16
+        assertSame(channelState, channelStateMap["#dumpsterdiving"])
17
+    }
18
+
19
+    @Test
20
+    fun `ChannelStateMap disallows setting the same channel twice`() {
21
+        val channelStateMap = ChannelStateMap { CaseMapping.Rfc }
22
+        channelStateMap += ChannelState("#dumpsterdiving")
23
+
24
+        assertThrows<IllegalArgumentException> {
25
+            channelStateMap += ChannelState("#DumpsterDiving")
26
+        }
27
+    }
28
+
29
+    @Test
30
+    fun `ChannelStateMap retrieves channels in different cases`() {
31
+        val channelStateMap = ChannelStateMap { CaseMapping.Rfc }
32
+        val channelState = ChannelState("#dumpsterdiving[]")
33
+        channelStateMap += channelState
34
+
35
+        assertSame(channelState, channelStateMap["#dumpsterdiving{}"])
36
+    }
37
+
38
+    @Test
39
+    fun `ChannelStateMap returns null if channel not found`() {
40
+        val channelStateMap = ChannelStateMap { CaseMapping.Ascii }
41
+        val channelState = ChannelState("#dumpsterdiving[]")
42
+        channelStateMap += channelState
43
+
44
+        assertNull(channelStateMap["#dumpsterdiving{}"])
45
+    }
46
+
47
+    @Test
48
+    fun `ChannelStateMap removes channels`() {
49
+        val channelStateMap = ChannelStateMap { CaseMapping.Rfc }
50
+        val channelState = ChannelState("#dumpsterdiving")
51
+        channelStateMap += channelState
52
+        channelStateMap -= ChannelState("#dumpsterDIVING")
53
+
54
+        assertNull(channelStateMap["#dumpsterdiving"])
55
+    }
56
+
57
+    @Test
58
+    fun `ChannelStateMap can be iterated`() {
59
+        val channelStateMap = ChannelStateMap { CaseMapping.Rfc }
60
+        channelStateMap += ChannelState("#dumpsterdiving")
61
+        channelStateMap += ChannelState("#gibson")
62
+
63
+        val names = channelStateMap.map { it.name }.toList()
64
+        assertEquals(2, names.size)
65
+        assertTrue(names.contains("#dumpsterdiving"))
66
+        assertTrue(names.contains("#gibson"))
67
+    }
68
+
69
+    @Test
70
+    fun `ChannelStateMap indicates if it contains a channel or not`() {
71
+        val channelStateMap = ChannelStateMap { CaseMapping.Rfc }
72
+        channelStateMap += ChannelState("#dumpsterdiving")
73
+
74
+        assertTrue("#dumpsterDIVING" in channelStateMap)
75
+        assertFalse("#crashandburn" in channelStateMap)
76
+    }
77
+
78
+}

+ 33
- 0
src/test/kotlin/com/dmdirc/ktirc/model/UserTest.kt View File

@@ -0,0 +1,33 @@
1
+package com.dmdirc.ktirc.model
2
+
3
+import org.junit.jupiter.api.Assertions.assertEquals
4
+import org.junit.jupiter.api.Assertions.assertNull
5
+import org.junit.jupiter.api.Test
6
+
7
+internal class UserTest {
8
+
9
+    @Test
10
+    fun `ByteArray asUser returns user with just nickname`() {
11
+        val user = "acidBurn".toByteArray().asUser()
12
+        assertEquals("acidBurn", user.nickname)
13
+        assertNull(user.ident)
14
+        assertNull(user.hostname)
15
+    }
16
+
17
+    @Test
18
+    fun `ByteArray asUser returns user and ident`() {
19
+        val user = "acidBurn!libby".toByteArray().asUser()
20
+        assertEquals("acidBurn", user.nickname)
21
+        assertEquals("libby", user.ident)
22
+        assertNull(user.hostname)
23
+    }
24
+
25
+    @Test
26
+    fun `ByteArray asUser returns user ident and host`() {
27
+        val user = "acidBurn!libby@root.localhost".toByteArray().asUser()
28
+        assertEquals("acidBurn", user.nickname)
29
+        assertEquals("libby", user.ident)
30
+        assertEquals("root.localhost", user.hostname)
31
+    }
32
+
33
+}

Loading…
Cancel
Save