Browse Source

Fix handling of channel user modes

Closes #8
tags/v0.6.0
Chris Smith 5 years ago
parent
commit
1d119f3166

+ 1
- 0
CHANGELOG View File

@@ -3,6 +3,7 @@ vNEXT (in development)
3 3
  * Changed USER command to not send the server name, per modern standards
4 4
  * Added support for SASL authentication (with PLAIN mechanism)
5 5
  * Removed some unused test code
6
+ * Fixed handling of user mode changes on channels (op/deop/etc)
6 7
  * Message extensions:
7 8
     * Added support for IRCv3 message tags v3.3
8 9
     * Exposed message IDs in MessageReceived and ActionReceived events

+ 26
- 8
src/main/kotlin/com/dmdirc/ktirc/events/ChannelStateHandler.kt View File

@@ -75,7 +75,7 @@ internal class ChannelStateHandler : EventHandler {
75 75
     }
76 76
 
77 77
     private fun handleModeChanged(client: IrcClient, event: ModeChanged) {
78
-        val chan =  client.channelState[event.target] ?: return
78
+        val chan = client.channelState[event.target] ?: return
79 79
         if (event.discovered) {
80 80
             chan.modesDiscovered = true
81 81
             chan.modes.clear()
@@ -93,15 +93,33 @@ internal class ChannelStateHandler : EventHandler {
93 93
     }
94 94
 
95 95
     private fun adjustMode(client: IrcClient, chan: ChannelState, mode: Char, arguments: Array<String>, argumentOffset: Int, adding: Boolean): Int {
96
-        val type = client.serverState.channelModeType(mode)
97
-        val takesParam = if (adding) type.needsParameterToSet else type.needsParameterToUnset
98
-        val param = if (takesParam) arguments[argumentOffset] else ""
99
-        if (adding) {
100
-            chan.modes[mode] = param
96
+        return if (client.serverState.isChannelUserMode(mode)) {
97
+            adjustUserMode(client, chan, mode, adding, arguments[argumentOffset])
98
+            1
101 99
         } else {
102
-            chan.modes.remove(mode)
100
+            val type = client.serverState.channelModeType(mode)
101
+            val takesParam = if (adding) type.needsParameterToSet else type.needsParameterToUnset
102
+            val param = if (takesParam) arguments[argumentOffset] else ""
103
+            if (adding) {
104
+                chan.modes[mode] = param
105
+            } else {
106
+                chan.modes.remove(mode)
107
+            }
108
+            if (takesParam) 1 else 0
109
+        }
110
+    }
111
+
112
+    private fun adjustUserMode(client: IrcClient, chan: ChannelState, mode: Char, adding: Boolean, user: String) {
113
+        chan.users[user]?.let { channelUser ->
114
+            // Filter from the master list of mode prefixes so that ordering is consistent
115
+            channelUser.modes = client.serverState.channelModePrefixes.modes.filter {
116
+                if (adding) {
117
+                    it == mode || it in channelUser.modes
118
+                } else {
119
+                    it != mode && it in channelUser.modes
120
+                }
121
+            }
103 122
         }
104
-        return if (takesParam) 1 else 0
105 123
     }
106 124
 
107 125
     private fun handleQuit(client: IrcClient, event: UserQuit) = sequence {

+ 13
- 0
src/main/kotlin/com/dmdirc/ktirc/model/ServerState.kt View File

@@ -52,6 +52,17 @@ class ServerState internal constructor(
52 52
     /** The current state of SASL authentication. */
53 53
     internal val sasl = SaslState(saslMechanisms)
54 54
 
55
+    /**
56
+     * Convenience accessor for the [ServerFeature.ModePrefixes] feature, which will always have a value.
57
+     */
58
+    val channelModePrefixes
59
+        get() = features[ServerFeature.ModePrefixes] ?: throw IllegalStateException("lost mode prefixes")
60
+
61
+    /**
62
+     * Determines if the given mode is one applied to a user of a channel, such as 'o' for operator.
63
+     */
64
+    fun isChannelUserMode(mode: Char) = channelModePrefixes.isMode(mode)
65
+
55 66
     /**
56 67
      * Determines what type of channel mode the given character is, based on the server features.
57 68
      *
@@ -103,6 +114,8 @@ data class ModePrefixMapping(val modes: String, val prefixes: String) {
103 114
 
104 115
     /** Determines whether the given character is a mode prefix (e.g. "@", "+"). */
105 116
     fun isPrefix(char: Char) = prefixes.contains(char)
117
+    /** Determines whether the given character is a channel user mode (e.g. "o", "v"). */
118
+    fun isMode(char: Char) = modes.contains(char)
106 119
     /** Gets the mode corresponding to the given prefix (e.g. "@" -> "o"). */
107 120
     fun getMode(prefix: Char) = modes[prefixes.indexOf(prefix)]
108 121
     /** Gets the modes corresponding to the given prefixes (e.g. "@+" -> "ov"). */

+ 75
- 1
src/test/kotlin/com/dmdirc/ktirc/events/ChannelStateHandlerTest.kt View File

@@ -260,5 +260,79 @@ internal class ChannelStateHandlerTest {
260 260
         assertNull(channel.modes['h'])
261 261
     }
262 262
 
263
+    @Test
264
+    fun `handles unprivileged user gaining new mode`() {
265
+        with (ChannelState("#thegibson") { CaseMapping.Rfc }) {
266
+            users += ChannelUser("ZeroCool")
267
+            channelStateMap += this
268
+        }
269
+
270
+        handler.processEvent(ircClient, ModeChanged(TestConstants.time, "#thegibson", "+o", arrayOf("zeroCool")))
271
+
272
+        assertEquals("o", channelStateMap["#thegibson"]?.users?.get("zeroCool")?.modes)
273
+    }
274
+
275
+    @Test
276
+    fun `handles privileged user gaining lesser mode`() {
277
+        with (ChannelState("#thegibson") { CaseMapping.Rfc }) {
278
+            users += ChannelUser("ZeroCool", "o")
279
+            channelStateMap += this
280
+        }
281
+
282
+        handler.processEvent(ircClient, ModeChanged(TestConstants.time, "#thegibson", "+v", arrayOf("zeroCool")))
283
+
284
+        assertEquals("ov", channelStateMap["#thegibson"]?.users?.get("zeroCool")?.modes)
285
+    }
286
+
287
+    @Test
288
+    fun `handles privileged user gaining greater mode`() {
289
+        with (ChannelState("#thegibson") { CaseMapping.Rfc }) {
290
+            users += ChannelUser("ZeroCool", "v")
291
+            channelStateMap += this
292
+        }
293
+
294
+        handler.processEvent(ircClient, ModeChanged(TestConstants.time, "#thegibson", "+o", arrayOf("zeroCool")))
295
+
296
+        assertEquals("ov", channelStateMap["#thegibson"]?.users?.get("zeroCool")?.modes)
297
+    }
298
+
299
+    @Test
300
+    fun `handles user gaining multiple modes`() {
301
+        with (ChannelState("#thegibson") { CaseMapping.Rfc }) {
302
+            users += ChannelUser("ZeroCool")
303
+            channelStateMap += this
304
+        }
305
+
306
+        handler.processEvent(ircClient, ModeChanged(TestConstants.time, "#thegibson", "+vo", arrayOf("zeroCool", "zeroCool")))
307
+
308
+        assertEquals("ov", channelStateMap["#thegibson"]?.users?.get("zeroCool")?.modes)
309
+    }
310
+
311
+    @Test
312
+    fun `handles user losing multiple modes`() {
313
+        with (ChannelState("#thegibson") { CaseMapping.Rfc }) {
314
+            users += ChannelUser("ZeroCool", "ov")
315
+            channelStateMap += this
316
+        }
317
+
318
+        handler.processEvent(ircClient, ModeChanged(TestConstants.time, "#thegibson", "-vo", arrayOf("zeroCool", "zeroCool")))
319
+
320
+        assertEquals("", channelStateMap["#thegibson"]?.users?.get("zeroCool")?.modes)
321
+    }
322
+
323
+    @Test
324
+    fun `handles mixture of user modes and normal modes`() {
325
+        with (ChannelState("#thegibson") { CaseMapping.Rfc }) {
326
+            users += ChannelUser("ZeroCool", "v")
327
+            channelStateMap += this
328
+        }
329
+        serverState.features[ServerFeature.ChannelModes] = arrayOf("ab", "cd", "ef", "gh")
330
+
331
+        handler.processEvent(ircClient, ModeChanged(TestConstants.time, "#thegibson", "oa-v+b", arrayOf("zeroCool", "aaa", "zeroCool", "bbb")))
332
+
333
+        assertEquals("o", channelStateMap["#thegibson"]?.users?.get("zeroCool")?.modes)
334
+        assertEquals("aaa", channelStateMap["#thegibson"]?.modes?.get('a'))
335
+        assertEquals("bbb", channelStateMap["#thegibson"]?.modes?.get('b'))
336
+    }
263 337
 
264
-}
338
+}

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

@@ -33,6 +33,18 @@ internal class ServerStateTest {
33 33
         assertEquals(ChannelModeType.NoParameter, serverState.channelModeType('g'))
34 34
     }
35 35
 
36
+    @Test
37
+    fun `returns whether a mode is a channel user mode or not`() {
38
+        val serverState = ServerState("acidBurn", "")
39
+        serverState.features[ServerFeature.ModePrefixes] = ModePrefixMapping("oqv", "@~+")
40
+        assertTrue(serverState.isChannelUserMode('o'))
41
+        assertTrue(serverState.isChannelUserMode('q'))
42
+        assertTrue(serverState.isChannelUserMode('v'))
43
+        assertFalse(serverState.isChannelUserMode('@'))
44
+        assertFalse(serverState.isChannelUserMode('!'))
45
+        assertFalse(serverState.isChannelUserMode('z'))
46
+    }
47
+
36 48
     @Test
37 49
     fun `returns NoParameter for unknown channel mode`() {
38 50
         val serverState = ServerState("acidBurn", "")
@@ -59,6 +71,15 @@ internal class ModePrefixMappingTest {
59 71
         assertFalse(mapping.isPrefix('o'))
60 72
     }
61 73
 
74
+    @Test
75
+    fun `ModePrefixMapping identifies which chars are modes`() {
76
+        val mapping = ModePrefixMapping("oav", "+@-")
77
+        assertFalse(mapping.isMode('+'))
78
+        assertFalse(mapping.isMode('@'))
79
+        assertFalse(mapping.isMode('!'))
80
+        assertTrue(mapping.isMode('o'))
81
+    }
82
+
62 83
     @Test
63 84
     fun `ModePrefixMapping maps prefixes to modes`() {
64 85
         val mapping = ModePrefixMapping("oav", "+@-")

Loading…
Cancel
Save