Browse Source

Add kick processing and event

Closes #7
tags/v0.5.0
Chris Smith 5 years ago
parent
commit
4a6ff8d7c6

+ 1
- 0
CHANGELOG View File

@@ -12,6 +12,7 @@ vNEXT (in development)
12 12
  * Other new events:
13 13
     * Added MotdFinished event
14 14
     * Added UserAccountChanged event
15
+    * Added ChannelUserKicked event
15 16
  * Improved some documentation
16 17
 
17 18
 v0.4.0

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

@@ -69,7 +69,12 @@ interface IrcClient {
69 69
     /**
70 70
      * Utility method to determine if the given user is the one we are connected to IRC as.
71 71
      */
72
-    fun isLocalUser(user: User) = caseMapping.areEquivalent(user.nickname, serverState.localNickname)
72
+    fun isLocalUser(user: User) = isLocalUser(user.nickname)
73
+
74
+    /**
75
+     * Utility method to determine if the given user is the one we are connected to IRC as.
76
+     */
77
+    fun isLocalUser(nickname: String) = caseMapping.areEquivalent(nickname, serverState.localNickname)
73 78
 
74 79
 }
75 80
 

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

@@ -15,6 +15,7 @@ internal class ChannelStateHandler : EventHandler {
15 15
             is ChannelParted -> handlePart(client, event)
16 16
             is ChannelNamesReceived -> handleNamesReceived(client, event)
17 17
             is ChannelNamesFinished -> handleNamesFinished(client, event)
18
+            is ChannelUserKicked -> handleKick(client, event)
18 19
             is ModeChanged -> handleModeChanged(client, event)
19 20
             is UserQuit -> return handleQuit(client, event)
20 21
         }
@@ -41,6 +42,17 @@ internal class ChannelStateHandler : EventHandler {
41 42
         }
42 43
     }
43 44
 
45
+    private fun handleKick(client: IrcClient, event: ChannelUserKicked) {
46
+        if (client.isLocalUser(event.victim)) {
47
+            log.info { "Kicked from channel: ${event.channel}" }
48
+            client.channelState -= event.channel
49
+        } else {
50
+            client.channelState[event.channel]?.let {
51
+                it.users -= event.victim
52
+            }
53
+        }
54
+    }
55
+
44 56
     private fun handleNamesReceived(client: IrcClient, event: ChannelNamesReceived) {
45 57
         val channel = client.channelState[event.channel] ?: return
46 58
 

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

@@ -35,6 +35,9 @@ class ChannelJoined(time: LocalDateTime, val user: User, val channel: String) :
35 35
 /** Raised when a user leaves a channel. */
36 36
 class ChannelParted(time: LocalDateTime, val user: User, val channel: String, val reason: String = "") : IrcEvent(time)
37 37
 
38
+/** Raised when a [victim] is kicked from a channel. */
39
+class ChannelUserKicked(time: LocalDateTime, val user: User, val channel: String, val victim: String, val reason: String = ""): IrcEvent(time)
40
+
38 41
 /** Raised when a user quits, and is in a channel. */
39 42
 class ChannelQuit(time: LocalDateTime, val user: User, val channel: String, val reason: String = "") : IrcEvent(time)
40 43
 

+ 16
- 0
src/main/kotlin/com/dmdirc/ktirc/events/UserStateHandler.kt View File

@@ -9,6 +9,7 @@ internal class UserStateHandler : EventHandler {
9 9
         when (event) {
10 10
             is ChannelJoined -> handleJoin(client.userState, event)
11 11
             is ChannelParted -> handlePart(client, event)
12
+            is ChannelUserKicked -> handleKick(client, event)
12 13
             is ChannelNamesReceived  -> handleNamesReceived(client, event)
13 14
             is UserAccountChanged -> handleAccountChanged(client, event)
14 15
             is UserQuit -> handleQuit(client.userState, event)
@@ -36,6 +37,21 @@ internal class UserStateHandler : EventHandler {
36 37
         }
37 38
     }
38 39
 
40
+    private fun handleKick(client: IrcClient, event: ChannelUserKicked) {
41
+        if (client.isLocalUser(event.victim)) {
42
+            // Remove channel from all users
43
+            client.userState.forEach { it.channels -= event.channel }
44
+            client.userState.removeIf { it.channels.isEmpty() && !client.isLocalUser(it.details) }
45
+        } else {
46
+            client.userState[event.victim]?.channels?.let {
47
+                it -= event.channel
48
+                if (it.isEmpty()) {
49
+                    client.userState -= event.victim
50
+                }
51
+            }
52
+        }
53
+    }
54
+
39 55
     private fun handleNamesReceived(client: IrcClient, event: ChannelNamesReceived) {
40 56
         event.toModesAndUsers(client).forEach { (_, user) ->
41 57
             client.userState.addToChannel(user, event.channel)

+ 23
- 0
src/main/kotlin/com/dmdirc/ktirc/messages/KickProcessor.kt View File

@@ -0,0 +1,23 @@
1
+package com.dmdirc.ktirc.messages
2
+
3
+import com.dmdirc.ktirc.events.ChannelUserKicked
4
+import com.dmdirc.ktirc.model.IrcMessage
5
+
6
+internal class KickProcessor : MessageProcessor {
7
+
8
+    override val commands = arrayOf("KICK")
9
+
10
+    override fun process(message: IrcMessage) = message.sourceUser?.let { user ->
11
+        listOf(ChannelUserKicked(message.time, user, message.channel, message.victim, message.reason))
12
+    } ?: emptyList()
13
+
14
+    private val IrcMessage.channel
15
+        get() = String(params[0])
16
+
17
+    private val IrcMessage.victim
18
+        get() = String(params[1])
19
+
20
+    private val IrcMessage.reason
21
+        get() = if (params.size > 2) String(params[2]) else ""
22
+
23
+}

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

@@ -22,6 +22,7 @@ internal val messageProcessors = setOf(
22 22
         CapabilityProcessor(),
23 23
         ISupportProcessor(),
24 24
         JoinProcessor(),
25
+        KickProcessor(),
25 26
         ModeProcessor(),
26 27
         MotdProcessor(),
27 28
         NamesProcessor(),

+ 1
- 0
src/main/kotlin/com/dmdirc/ktirc/model/UserState.kt View File

@@ -16,6 +16,7 @@ class UserState(private val caseMappingProvider: () -> CaseMapping): Iterable<Kn
16 16
 
17 17
     internal operator fun plusAssign(details: User) { users += KnownUser(caseMappingProvider, details) }
18 18
     internal operator fun minusAssign(details: User) { users -= details.nickname }
19
+    internal operator fun minusAssign(nickname: String) { users -= nickname }
19 20
 
20 21
     /** Provides a read-only iterator of all users. */
21 22
     override operator fun iterator() = users.iterator().iterator()

+ 9
- 0
src/test/kotlin/com/dmdirc/ktirc/IrcClientTest.kt View File

@@ -163,6 +163,15 @@ internal class IrcClientImplTest {
163 163
         assertFalse(client.isLocalUser(User("acid-Burn", "libby", "root.localhost")))
164 164
     }
165 165
 
166
+    @Test
167
+    fun `IrcClient indicates if nickname is local user or not`() {
168
+        val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))
169
+        client.serverState.localNickname = "[acidBurn]"
170
+
171
+        assertTrue(client.isLocalUser("{acidBurn}"))
172
+        assertFalse(client.isLocalUser("acid-Burn"))
173
+    }
174
+
166 175
     @Test
167 176
     fun `IrcClient uses current case mapping to check local user`() {
168 177
         val client = IrcClientImpl(Server(HOST, PORT), Profile(NICK, REAL_NAME, USER_NAME))

+ 34
- 12
src/test/kotlin/com/dmdirc/ktirc/events/ChannelStateHandlerTest.kt View File

@@ -18,22 +18,23 @@ internal class ChannelStateHandlerTest {
18 18
         on { serverState } doReturn serverState
19 19
         on { channelState } doReturn channelStateMap
20 20
         on { isLocalUser(User("acidburn", "libby", "root.localhost")) } doReturn true
21
+        on { isLocalUser("acidburn") } doReturn  true
21 22
     }
22 23
 
23 24
     @Test
24
-    fun `ChannelStateHandler creates new state object for local joins`() {
25
+    fun `creates new state object for local joins`() {
25 26
         handler.processEvent(ircClient, ChannelJoined(TestConstants.time, User("acidburn", "libby", "root.localhost"), "#thegibson"))
26 27
         assertTrue("#thegibson" in channelStateMap)
27 28
     }
28 29
 
29 30
     @Test
30
-    fun `ChannelStateHandler does not create new state object for remote joins`() {
31
+    fun `does not create new state object for remote joins`() {
31 32
         handler.processEvent(ircClient, ChannelJoined(TestConstants.time, User("zerocool", "dade", "root.localhost"), "#thegibson"))
32 33
         assertFalse("#thegibson" in channelStateMap)
33 34
     }
34 35
 
35 36
     @Test
36
-    fun `ChannelStateHandler adds joiners to channel state`() {
37
+    fun `adds joiners to channel state`() {
37 38
         channelStateMap += ChannelState("#thegibson") { CaseMapping.Rfc }
38 39
 
39 40
         handler.processEvent(ircClient, ChannelJoined(TestConstants.time, User("zerocool", "dade", "root.localhost"), "#thegibson"))
@@ -42,7 +43,7 @@ internal class ChannelStateHandlerTest {
42 43
     }
43 44
 
44 45
     @Test
45
-    fun `ChannelStateHandler clears existing users when getting a new list`() {
46
+    fun `clears existing users when getting a new list`() {
46 47
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
47 48
         channel.users += ChannelUser("acidBurn")
48 49
         channel.users += ChannelUser("thePlague")
@@ -55,7 +56,7 @@ internal class ChannelStateHandlerTest {
55 56
     }
56 57
 
57 58
     @Test
58
-    fun `ChannelStateHandler adds users from multiple name received events`() {
59
+    fun `adds users from multiple name received events`() {
59 60
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
60 61
         channelStateMap += channel
61 62
 
@@ -70,7 +71,7 @@ internal class ChannelStateHandlerTest {
70 71
     }
71 72
 
72 73
     @Test
73
-    fun `ChannelStateHandler clears and readds users on additional names received`() {
74
+    fun `clears and readds users on additional names received`() {
74 75
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
75 76
         channelStateMap += channel
76 77
 
@@ -85,7 +86,7 @@ internal class ChannelStateHandlerTest {
85 86
     }
86 87
 
87 88
     @Test
88
-    fun `ChannelStateHandler adds users with mode prefixes`() {
89
+    fun `adds users with mode prefixes`() {
89 90
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
90 91
         channelStateMap += channel
91 92
         serverState.features[ServerFeature.ModePrefixes] = ModePrefixMapping("ov", "@+")
@@ -101,7 +102,7 @@ internal class ChannelStateHandlerTest {
101 102
     }
102 103
 
103 104
     @Test
104
-    fun `ChannelStateHandler adds users with full hosts`() {
105
+    fun `adds users with full hosts`() {
105 106
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
106 107
         channelStateMap += channel
107 108
         serverState.features[ServerFeature.ModePrefixes] = ModePrefixMapping("ov", "@+")
@@ -115,7 +116,7 @@ internal class ChannelStateHandlerTest {
115 116
     }
116 117
 
117 118
     @Test
118
-    fun `ChannelStateHandler removes state object for local parts`() {
119
+    fun `removes state object for local parts`() {
119 120
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
120 121
         channelStateMap += channel
121 122
 
@@ -125,7 +126,7 @@ internal class ChannelStateHandlerTest {
125 126
     }
126 127
 
127 128
     @Test
128
-    fun `ChannelStateHandler removes user from channel member list for remote parts`() {
129
+    fun `removes user from channel member list for remote parts`() {
129 130
         val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
130 131
         channel.users += ChannelUser("ZeroCool")
131 132
         channelStateMap += channel
@@ -136,7 +137,28 @@ internal class ChannelStateHandlerTest {
136 137
     }
137 138
 
138 139
     @Test
139
-    fun `ChannelStateHandler removes user from all channel member lists for quits`() {
140
+    fun `removes state object for local kicks`() {
141
+        val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
142
+        channelStateMap += channel
143
+
144
+        handler.processEvent(ircClient, ChannelUserKicked(TestConstants.time, User("zerocool", "dade", "root.localhost"), "#thegibson", "acidburn", "Bye!"))
145
+
146
+        assertFalse("#thegibson" in channelStateMap)
147
+    }
148
+
149
+    @Test
150
+    fun `removes user from channel member list for remote kicks`() {
151
+        val channel = ChannelState("#thegibson") { CaseMapping.Rfc }
152
+        channel.users += ChannelUser("ZeroCool")
153
+        channelStateMap += channel
154
+
155
+        handler.processEvent(ircClient, ChannelUserKicked(TestConstants.time, User("acidburn", "libby", "root.localhost"), "#thegibson", "zerocool", "Bye!"))
156
+
157
+        assertFalse("zerocool" in channel.users)
158
+    }
159
+
160
+    @Test
161
+    fun `removes user from all channel member lists for quits`() {
140 162
         with (ChannelState("#thegibson") { CaseMapping.Rfc }) {
141 163
             users += ChannelUser("ZeroCool")
142 164
             channelStateMap += this
@@ -162,7 +184,7 @@ internal class ChannelStateHandlerTest {
162 184
 
163 185
 
164 186
     @Test
165
-    fun `ChannelStateHandler raises ChannelQuit event for each channel a user quits from`() {
187
+    fun `raises ChannelQuit event for each channel a user quits from`() {
166 188
         with (ChannelState("#thegibson") { CaseMapping.Rfc }) {
167 189
             users += ChannelUser("ZeroCool")
168 190
             channelStateMap += this

+ 70
- 1
src/test/kotlin/com/dmdirc/ktirc/events/UserStateHandlerTest.kt View File

@@ -20,7 +20,8 @@ internal class UserStateHandlerTest {
20 20
     private val ircClient = mock<IrcClient> {
21 21
         on { serverState } doReturn serverState
22 22
         on { userState } doReturn userState
23
-        on { isLocalUser(argForWhich { nickname == "zeroCool" }) } doReturn true
23
+        on { isLocalUser(argForWhich<User> { nickname == "zeroCool" }) } doReturn true
24
+        on { isLocalUser("zeroCool") } doReturn true
24 25
     }
25 26
 
26 27
     private val handler = UserStateHandler()
@@ -121,6 +122,74 @@ internal class UserStateHandlerTest {
121 122
         }
122 123
     }
123 124
 
125
+
126
+    @Test
127
+    fun `removes channel from user on kick`() {
128
+        runBlocking {
129
+            userState += User("acidBurn")
130
+            userState.addToChannel(User("acidBurn"), "#thegibson")
131
+            userState.addToChannel(User("acidBurn"), "#dumpsterdiving")
132
+
133
+            handler.processEvent(ircClient, ChannelUserKicked(TestConstants.time, User("thePlague"), "#dumpsterdiving", "acidBurn"))
134
+
135
+            assertEquals(listOf("#thegibson"), userState["acidBurn"]?.channels?.toList())
136
+        }
137
+    }
138
+
139
+    @Test
140
+    fun `removes user on kick from last channel`() {
141
+        runBlocking {
142
+            userState += User("acidBurn")
143
+            userState.addToChannel(User("acidBurn"), "#dumpsterdiving")
144
+
145
+            handler.processEvent(ircClient, ChannelUserKicked(TestConstants.time, User("thePlague"), "#dumpsterdiving", "acidBurn"))
146
+
147
+            assertNull(userState["acidBurn"])
148
+        }
149
+    }
150
+
151
+    @Test
152
+    fun `removes channel from all users on local kick`() {
153
+        runBlocking {
154
+            userState += User("acidBurn")
155
+            userState.addToChannel(User("acidBurn"), "#dumpsterdiving")
156
+            userState.addToChannel(User("acidBurn"), "#thegibson")
157
+
158
+            userState += User("zeroCool")
159
+            userState.addToChannel(User("zeroCool"), "#dumpsterdiving")
160
+            userState.addToChannel(User("zeroCool"), "#thegibson")
161
+
162
+            handler.processEvent(ircClient, ChannelUserKicked(TestConstants.time, User("thePlague"), "#dumpsterdiving", "zeroCool"))
163
+
164
+            assertEquals(listOf("#thegibson"), userState["acidBurn"]?.channels?.toList())
165
+            assertEquals(listOf("#thegibson"), userState["zeroCool"]?.channels?.toList())
166
+        }
167
+    }
168
+
169
+    @Test
170
+    fun `removes remote users with no remaining channels on local kick`() {
171
+        runBlocking {
172
+            userState += User("acidBurn")
173
+            userState.addToChannel(User("acidBurn"), "#dumpsterdiving")
174
+
175
+            handler.processEvent(ircClient, ChannelUserKicked(TestConstants.time, User("thePlague"), "#dumpsterdiving", "zeroCool"))
176
+
177
+            assertNull(userState["acidBurn"])
178
+        }
179
+    }
180
+
181
+    @Test
182
+    fun `keeps local user with no remaining channels after local kick`() {
183
+        runBlocking {
184
+            userState += User("zeroCool")
185
+            userState.addToChannel(User("zeroCool"), "#dumpsterdiving")
186
+
187
+            handler.processEvent(ircClient, ChannelUserKicked(TestConstants.time, User("thePlague"), "#dumpsterdiving", "zeroCool"))
188
+
189
+            assertNotNull(userState["zeroCool"])
190
+        }
191
+    }
192
+
124 193
     @Test
125 194
     fun `removes user entirely on quit`() {
126 195
         runBlocking {

+ 52
- 0
src/test/kotlin/com/dmdirc/ktirc/messages/KickProcessorTest.kt View File

@@ -0,0 +1,52 @@
1
+package com.dmdirc.ktirc.messages
2
+
3
+import com.dmdirc.ktirc.TestConstants
4
+import com.dmdirc.ktirc.model.IrcMessage
5
+import com.dmdirc.ktirc.model.User
6
+import com.dmdirc.ktirc.params
7
+import com.dmdirc.ktirc.util.currentTimeProvider
8
+import org.junit.jupiter.api.Assertions.assertEquals
9
+import org.junit.jupiter.api.BeforeEach
10
+import org.junit.jupiter.api.Test
11
+
12
+internal class KickProcessorTest {
13
+
14
+    @BeforeEach
15
+    fun setUp() {
16
+        currentTimeProvider = { TestConstants.time }
17
+    }
18
+
19
+    @Test
20
+    fun `raises kick event without message`() {
21
+        val events = KickProcessor().process(
22
+                IrcMessage(emptyMap(), "acidburn!libby@root.localhost".toByteArray(), "KICK", params("#crashandburn", "zeroCool")))
23
+        assertEquals(1, events.size)
24
+
25
+        assertEquals(TestConstants.time, events[0].time)
26
+        assertEquals(User("acidburn", "libby", "root.localhost"), events[0].user)
27
+        assertEquals("#crashandburn", events[0].channel)
28
+        assertEquals("zeroCool", events[0].victim)
29
+        assertEquals("", events[0].reason)
30
+    }
31
+
32
+    @Test
33
+    fun `raises kick event with message`() {
34
+        val events = KickProcessor().process(
35
+                IrcMessage(emptyMap(), "acidburn!libby@root.localhost".toByteArray(), "KICK", params("#crashandburn", "zeroCool", "Hack the planet!")))
36
+        assertEquals(1, events.size)
37
+
38
+        assertEquals(TestConstants.time, events[0].time)
39
+        assertEquals(User("acidburn", "libby", "root.localhost"), events[0].user)
40
+        assertEquals("#crashandburn", events[0].channel)
41
+        assertEquals("zeroCool", events[0].victim)
42
+        assertEquals("Hack the planet!", events[0].reason)
43
+    }
44
+
45
+    @Test
46
+    fun `does nothing if prefix missing`() {
47
+        val events = KickProcessor().process(
48
+                IrcMessage(emptyMap(), null, "KICK", params("#crashandburn", "zeroCool")))
49
+        assertEquals(0, events.size)
50
+    }
51
+
52
+}

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

@@ -25,6 +25,13 @@ internal class UserStateTest {
25 25
         assertNull(userState["acidburn"])
26 26
     }
27 27
 
28
+    @Test
29
+    fun `UserState removes users by nickname`() {
30
+        userState += User("acidBurn", "libby", "root.localhost")
31
+        userState -= "ACIDBURN"
32
+        assertNull(userState["acidburn"])
33
+    }
34
+
28 35
     @Test
29 36
     fun `UserState updates existing user with same nickname`() {
30 37
         userState += User("acidBurn", "libby", "root.localhost")

Loading…
Cancel
Save