Browse Source

Topic support

tags/v0.8.0
Chris Smith 3 months ago
parent
commit
511051ac4c

+ 1
- 0
CHANGELOG View File

@@ -2,6 +2,7 @@ vNEXT (in development)
2 2
 
3 3
  * Added support for SCRAM-SHA-1 and SCRAM-SHA-256 SASL mechanisms
4 4
  * Added MotdLineReceived event
5
+ * Added topic events and state
5 6
  * (Internal) Move event handlers into their own package
6 7
 
7 8
 v0.7.0

+ 1
- 1
gradle/wrapper/gradle-wrapper.properties View File

@@ -1,5 +1,5 @@
1 1
 distributionBase=GRADLE_USER_HOME
2 2
 distributionPath=wrapper/dists
3
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.2-bin.zip
3
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-bin.zip
4 4
 zipStoreBase=GRADLE_USER_HOME
5 5
 zipStorePath=wrapper/dists

+ 9
- 2
src/main/kotlin/com/dmdirc/ktirc/events/Events.kt View File

@@ -54,12 +54,19 @@ class ChannelNamesReceived(time: LocalDateTime, val channel: String, val names:
54 54
 /** Raised when the entirety of the channel's member list has been received. */
55 55
 class ChannelNamesFinished(time: LocalDateTime, val channel: String) : IrcEvent(time)
56 56
 
57
-/** Raised when a channel topic is discovered (not changed). Usually followed by [ChannelTopicMetadataDiscovered]. */
58
-class ChannelTopicDiscovered(time: LocalDateTime, val channel: String, val topic: String) : IrcEvent(time)
57
+/** Raised when a channel topic is discovered (not changed). Usually followed by [ChannelTopicMetadataDiscovered] if the [topic] is non-null. */
58
+class ChannelTopicDiscovered(time: LocalDateTime, val channel: String, val topic: String?) : IrcEvent(time)
59 59
 
60 60
 /** Raised when a channel topic's metadata is discovered. */
61 61
 class ChannelTopicMetadataDiscovered(time: LocalDateTime, val channel: String, val user: User, val setTime: LocalDateTime) : IrcEvent(time)
62 62
 
63
+/**
64
+ * Raised when a channel's topic is changed.
65
+ *
66
+ * If the topic has been unset (cleared), [topic] will be `null`
67
+ */
68
+class ChannelTopicChanged(time: LocalDateTime, val user: User, val channel: String, val topic: String?) : IrcEvent(time)
69
+
63 70
 /** Raised when a message is received. */
64 71
 class MessageReceived(time: LocalDateTime, val user: User, val target: String, val message: String, val messageId: String? = null) : IrcEvent(time)
65 72
 

+ 30
- 0
src/main/kotlin/com/dmdirc/ktirc/handlers/ChannelStateHandler.kt View File

@@ -3,6 +3,7 @@ package com.dmdirc.ktirc.handlers
3 3
 import com.dmdirc.ktirc.IrcClient
4 4
 import com.dmdirc.ktirc.events.*
5 5
 import com.dmdirc.ktirc.model.ChannelState
6
+import com.dmdirc.ktirc.model.ChannelTopic
6 7
 import com.dmdirc.ktirc.model.ChannelUser
7 8
 import com.dmdirc.ktirc.util.logger
8 9
 
@@ -17,6 +18,9 @@ internal class ChannelStateHandler : EventHandler {
17 18
             is ChannelNamesReceived -> handleNamesReceived(client, event)
18 19
             is ChannelNamesFinished -> handleNamesFinished(client, event)
19 20
             is ChannelUserKicked -> handleKick(client, event)
21
+            is ChannelTopicDiscovered -> handleTopicDiscovered(client, event)
22
+            is ChannelTopicMetadataDiscovered -> handleTopicMetadata(client, event)
23
+            is ChannelTopicChanged -> handleTopicChanged(client, event)
20 24
             is ModeChanged -> handleModeChanged(client, event)
21 25
             is UserQuit -> return handleQuit(client, event)
22 26
             is UserNickChanged -> return handleNickChanged(client, event)
@@ -76,6 +80,32 @@ internal class ChannelStateHandler : EventHandler {
76 80
         }
77 81
     }
78 82
 
83
+    private fun handleTopicDiscovered(client: IrcClient, event: ChannelTopicDiscovered) {
84
+        client.channelState[event.channel]?.let {
85
+            if (!it.topicDiscovered) {
86
+                it.topic = ChannelTopic(event.topic)
87
+                if (event.topic == null) {
88
+                    it.topicDiscovered = true
89
+                }
90
+            }
91
+        }
92
+    }
93
+
94
+    private fun handleTopicMetadata(client: IrcClient, event: ChannelTopicMetadataDiscovered) {
95
+        client.channelState[event.channel]?.let {
96
+            if (!it.topicDiscovered) {
97
+                it.topic = ChannelTopic(it.topic.topic, event.user, event.setTime)
98
+                it.topicDiscovered = true
99
+            }
100
+        }
101
+    }
102
+
103
+    private fun handleTopicChanged(client: IrcClient, event: ChannelTopicChanged) {
104
+        client.channelState[event.channel]?.let {
105
+            it.topic = ChannelTopic(event.topic, event.user, event.time)
106
+        }
107
+    }
108
+
79 109
     private fun handleModeChanged(client: IrcClient, event: ModeChanged) {
80 110
         val chan = client.channelState[event.target] ?: return
81 111
         if (event.discovered) {

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

@@ -8,6 +8,7 @@ internal const val RPL_ISUPPORT = "005"
8 8
 internal const val RPL_UMODEIS = "221"
9 9
 
10 10
 internal const val RPL_CHANNELMODEIS = "324"
11
+internal const val RPL_NOTOPIC = "331"
11 12
 internal const val RPL_TOPIC = "332"
12 13
 internal const val RPL_TOPICWHOTIME = "333"
13 14
 internal const val RPL_MOTD = "372"

+ 4
- 1
src/main/kotlin/com/dmdirc/ktirc/messages/TopicProcessor.kt View File

@@ -1,5 +1,6 @@
1 1
 package com.dmdirc.ktirc.messages
2 2
 
3
+import com.dmdirc.ktirc.events.ChannelTopicChanged
3 4
 import com.dmdirc.ktirc.events.ChannelTopicDiscovered
4 5
 import com.dmdirc.ktirc.events.ChannelTopicMetadataDiscovered
5 6
 import com.dmdirc.ktirc.model.IrcMessage
@@ -10,13 +11,15 @@ import java.time.LocalDateTime
10 11
 
11 12
 internal class TopicProcessor : MessageProcessor {
12 13
 
13
-    override val commands = arrayOf(RPL_TOPIC, RPL_TOPICWHOTIME)
14
+    override val commands = arrayOf(RPL_TOPIC, RPL_TOPICWHOTIME, RPL_NOTOPIC, "TOPIC")
14 15
 
15 16
     override fun process(message: IrcMessage) = sequence {
16 17
         when (message.command) {
17 18
             RPL_TOPIC -> yield(ChannelTopicDiscovered(message.time, message.channel, String(message.params[2])))
19
+            RPL_NOTOPIC -> yield(ChannelTopicDiscovered(message.time, message.channel, null))
18 20
             RPL_TOPICWHOTIME -> yield(ChannelTopicMetadataDiscovered(
19 21
                     message.time, message.channel, message.params[2].asUser(), message.topicSetTime))
22
+            "TOPIC" -> message.sourceUser?.let { yield(ChannelTopicChanged(message.time, it, String(message.params[0]), String(message.params[1]))) }
20 23
         }
21 24
     }.toList()
22 25
 

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

@@ -1,6 +1,7 @@
1 1
 package com.dmdirc.ktirc.model
2 2
 
3 3
 import com.dmdirc.ktirc.io.CaseMapping
4
+import java.time.LocalDateTime
4 5
 
5 6
 /**
6 7
  * Describes the state of a channel that the client has joined.
@@ -19,6 +20,17 @@ class ChannelState(val name: String, caseMappingProvider: () -> CaseMapping) {
19 20
     var modesDiscovered = false
20 21
         internal set
21 22
 
23
+    /**
24
+     * Whether or not we have discovered or seen a channel topic. Subsequent discoveries are ignored.
25
+     */
26
+    internal var topicDiscovered = false
27
+
28
+    /**
29
+     * The current channel topic.
30
+     */
31
+    var topic = ChannelTopic()
32
+        internal set
33
+
22 34
     /**
23 35
      * A map of all users in the channel to their current modes.
24 36
      */
@@ -34,11 +46,26 @@ class ChannelState(val name: String, caseMappingProvider: () -> CaseMapping) {
34 46
     internal fun reset() {
35 47
         receivingUserList = false
36 48
         modesDiscovered = false
49
+        topic = ChannelTopic()
50
+        topicDiscovered = false
37 51
         users.clear()
38 52
         modes.clear()
39 53
     }
40 54
 }
41 55
 
56
+/**
57
+ * Describes a channel topic, and when and by whom it was set.
58
+ *
59
+ * [topic] may be null if there is no topic set.
60
+ *
61
+ * [user] and [time] may not be known if the topic was set before we joined the channel, depending on when it
62
+ * is checked and the exact behaviour of the IRC server.
63
+ *
64
+ * If the topic is cleared while we are present, then [topic] will be `null` but the [user] and [time] that it
65
+ * was cleared will still be recorded.
66
+ */
67
+data class ChannelTopic(val topic: String? = null, val user: User? = null, val time: LocalDateTime? = null)
68
+
42 69
 /**
43 70
  * Describes a user in a channel, and their modes.
44 71
  */

+ 1
- 0
src/test/kotlin/com/dmdirc/ktirc/TestConstants.kt View File

@@ -4,4 +4,5 @@ import java.time.LocalDateTime
4 4
 
5 5
 object TestConstants {
6 6
     val time: LocalDateTime = LocalDateTime.parse("1995-09-15T09:00:00")
7
+    val otherTime : LocalDateTime = LocalDateTime.parse("1996-05-03T13:00:00")
7 8
 }

+ 67
- 0
src/test/kotlin/com/dmdirc/ktirc/handlers/ChannelStateHandlerTest.kt View File

@@ -383,4 +383,71 @@ internal class ChannelStateHandlerTest {
383 383
         assertEquals("bbb", channelStateMap["#thegibson"]?.modes?.get('b'))
384 384
     }
385 385
 
386
+    @Test
387
+    fun `updates topic state when it's discovered for the first time`() {
388
+        val state = ChannelState("#thegibson") { CaseMapping.Rfc }
389
+        channelStateMap += state
390
+
391
+        handler.processEvent(ircClient, ChannelTopicDiscovered(TestConstants.time, "#thegibson", "Hack the planet!"))
392
+        handler.processEvent(ircClient, ChannelTopicMetadataDiscovered(TestConstants.time, "#thegibson", User("acidBurn"), TestConstants.otherTime))
393
+
394
+        assertTrue(state.topicDiscovered)
395
+        assertEquals(ChannelTopic("Hack the planet!", User("acidBurn"), TestConstants.otherTime), state.topic)
396
+    }
397
+
398
+    @Test
399
+    fun `updates topic state when no topic is discovered for the first time`() {
400
+        val state = ChannelState("#thegibson") { CaseMapping.Rfc }
401
+        channelStateMap += state
402
+
403
+        handler.processEvent(ircClient, ChannelTopicDiscovered(TestConstants.time, "#thegibson", null))
404
+
405
+        assertTrue(state.topicDiscovered)
406
+        assertEquals(ChannelTopic(), state.topic)
407
+    }
408
+
409
+    @Test
410
+    fun `leaves topic state when it's discovered for a second time`() {
411
+        val state = ChannelState("#thegibson") { CaseMapping.Rfc }
412
+        state.topic = ChannelTopic("Hack the planet!", User("acidBurn"), TestConstants.otherTime)
413
+        state.topicDiscovered = true
414
+        channelStateMap += state
415
+
416
+        handler.processEvent(ircClient, ChannelTopicDiscovered(TestConstants.time, "#thegibson", "Hack the planet"))
417
+        handler.processEvent(ircClient, ChannelTopicMetadataDiscovered(TestConstants.time, "#thegibson", User("zeroCool"), TestConstants.time))
418
+
419
+        assertTrue(state.topicDiscovered)
420
+        assertEquals(ChannelTopic("Hack the planet!", User("acidBurn"), TestConstants.otherTime), state.topic)
421
+    }
422
+
423
+    @Test
424
+    fun `updates topic state when the topic is changed`() {
425
+        val state = ChannelState("#thegibson") { CaseMapping.Rfc }
426
+        channelStateMap += state
427
+
428
+        handler.processEvent(ircClient, ChannelTopicChanged(TestConstants.time, User("acidBurn"), "#thegibson", "Hack the planet!"))
429
+
430
+        assertEquals(ChannelTopic("Hack the planet!", User("acidBurn"), TestConstants.time), state.topic)
431
+    }
432
+
433
+    @Test
434
+    fun `updates topic state when the topic is unset`() {
435
+        val state = ChannelState("#thegibson") { CaseMapping.Rfc }
436
+        channelStateMap += state
437
+
438
+        handler.processEvent(ircClient, ChannelTopicChanged(TestConstants.time, User("acidBurn"), "#thegibson", null))
439
+
440
+        assertEquals(ChannelTopic(null, User("acidBurn"), TestConstants.time), state.topic)
441
+    }
442
+
443
+    @Test
444
+    fun `ignores topic change when channel doesn't exist`() {
445
+        val state = ChannelState("#thegibson") { CaseMapping.Rfc }
446
+        channelStateMap += state
447
+
448
+        handler.processEvent(ircClient, ChannelTopicChanged(TestConstants.time, User("acidBurn"), "#dumpsterdiving", "Hack the planet!"))
449
+
450
+        assertEquals(ChannelTopic(), state.topic)
451
+    }
452
+
386 453
 }

+ 36
- 5
src/test/kotlin/com/dmdirc/ktirc/messages/TopicProcessorTest.kt View File

@@ -1,16 +1,18 @@
1 1
 package com.dmdirc.ktirc.messages
2 2
 
3 3
 import com.dmdirc.ktirc.TestConstants
4
+import com.dmdirc.ktirc.events.ChannelTopicChanged
4 5
 import com.dmdirc.ktirc.events.ChannelTopicDiscovered
5 6
 import com.dmdirc.ktirc.events.ChannelTopicMetadataDiscovered
6 7
 import com.dmdirc.ktirc.model.IrcMessage
8
+import com.dmdirc.ktirc.model.User
7 9
 import com.dmdirc.ktirc.params
8 10
 import com.dmdirc.ktirc.util.currentTimeProvider
9 11
 import com.dmdirc.ktirc.util.currentTimeZoneProvider
10 12
 import org.junit.jupiter.api.Assertions.assertEquals
13
+import org.junit.jupiter.api.Assertions.assertNull
11 14
 import org.junit.jupiter.api.BeforeEach
12 15
 import org.junit.jupiter.api.Test
13
-import java.time.ZoneId
14 16
 
15 17
 internal class TopicProcessorTest {
16 18
 
@@ -33,17 +35,46 @@ internal class TopicProcessorTest {
33 35
     }
34 36
 
35 37
     @Test
36
-    fun `raises ChannelTopicMetadataDiscovered event when topic is supplied`() {
37
-        val events = processor.process(IrcMessage(emptyMap(), null, "333", params("acidBurn", "#thegibson", "zeroCool", unixtime(currentTimeZoneProvider()))))
38
+    fun `raises ChannelTopicDiscovered event when no topic is set`() {
39
+        val events = processor.process(IrcMessage(emptyMap(), null, "331", params("acidBurn", "#thegibson", "No topic set")))
40
+        assertEquals(1, events.size)
41
+
42
+        val event = events[0] as ChannelTopicDiscovered
43
+        assertEquals(TestConstants.time, event.time)
44
+        assertEquals("#thegibson", event.channel)
45
+        assertNull(event.topic)
46
+    }
47
+
48
+    @Test
49
+    fun `raises ChannelTopicMetadataDiscovered event when metadata is supplied`() {
50
+        val events = processor.process(IrcMessage(emptyMap(), null, "333", params("acidBurn", "#thegibson", "zeroCool", unixtime())))
38 51
         assertEquals(1, events.size)
39 52
 
40 53
         val event = events[0] as ChannelTopicMetadataDiscovered
41 54
         assertEquals(TestConstants.time, event.time)
42 55
         assertEquals("#thegibson", event.channel)
43 56
         assertEquals("zeroCool", event.user.nickname)
44
-        assertEquals(TestConstants.time, event.setTime)
57
+        assertEquals(TestConstants.otherTime, event.setTime)
58
+    }
59
+
60
+    @Test
61
+    fun `raises ChannelTopicChanged event when topic is changed`() {
62
+        val events = processor.process(IrcMessage(emptyMap(), "acidBurn!acidB@the.gibson".toByteArray(), "TOPIC", params("#thegibson", "Hack the planet!")))
63
+        assertEquals(1, events.size)
64
+
65
+        val event = events[0] as ChannelTopicChanged
66
+        assertEquals(TestConstants.time, event.time)
67
+        assertEquals(User("acidBurn", "acidB", "the.gibson"), event.user)
68
+        assertEquals("#thegibson", event.channel)
69
+        assertEquals("Hack the planet!", event.topic)
70
+    }
71
+
72
+    @Test
73
+    fun `does nothing when topic is changed with no source`() {
74
+        val events = processor.process(IrcMessage(emptyMap(), null, "TOPIC", params("#thegibson", "Hack the planet!")))
75
+        assertEquals(0, events.size)
45 76
     }
46 77
 
47
-    private fun unixtime(zoneId: ZoneId) = TestConstants.time.toEpochSecond(zoneId.rules.getOffset(TestConstants.time)).toString()
78
+    private fun unixtime() = TestConstants.otherTime.toEpochSecond(currentTimeZoneProvider().rules.getOffset(TestConstants.time)).toString()
48 79
 
49 80
 }

+ 4
- 0
src/test/kotlin/com/dmdirc/ktirc/model/ChannelStateTest.kt View File

@@ -26,15 +26,19 @@ internal class ChannelStateTest {
26 26
     fun `reset resets all state`() = with(ChannelState("#thegibson") { CaseMapping.Rfc }) {
27 27
         receivingUserList = true
28 28
         modesDiscovered = true
29
+        topicDiscovered = true
29 30
         modes['a'] = "b"
30 31
         users += ChannelUser("acidBurn")
32
+        topic = ChannelTopic("Hack the planet!")
31 33
 
32 34
         reset()
33 35
 
34 36
         assertFalse(receivingUserList)
35 37
         assertFalse(modesDiscovered)
38
+        assertFalse(topicDiscovered)
36 39
         assertTrue(modes.isEmpty())
37 40
         assertEquals(0, users.count())
41
+        assertEquals(ChannelTopic(), topic)
38 42
     }
39 43
 
40 44
 }

Loading…
Cancel
Save