Browse Source

Topic support

tags/v0.8.0
Chris Smith 5 years ago
parent
commit
511051ac4c

+ 1
- 0
CHANGELOG View File

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

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

1
 distributionBase=GRADLE_USER_HOME
1
 distributionBase=GRADLE_USER_HOME
2
 distributionPath=wrapper/dists
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
 zipStoreBase=GRADLE_USER_HOME
4
 zipStoreBase=GRADLE_USER_HOME
5
 zipStorePath=wrapper/dists
5
 zipStorePath=wrapper/dists

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

54
 /** Raised when the entirety of the channel's member list has been received. */
54
 /** Raised when the entirety of the channel's member list has been received. */
55
 class ChannelNamesFinished(time: LocalDateTime, val channel: String) : IrcEvent(time)
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
 /** Raised when a channel topic's metadata is discovered. */
60
 /** Raised when a channel topic's metadata is discovered. */
61
 class ChannelTopicMetadataDiscovered(time: LocalDateTime, val channel: String, val user: User, val setTime: LocalDateTime) : IrcEvent(time)
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
 /** Raised when a message is received. */
70
 /** Raised when a message is received. */
64
 class MessageReceived(time: LocalDateTime, val user: User, val target: String, val message: String, val messageId: String? = null) : IrcEvent(time)
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
 import com.dmdirc.ktirc.IrcClient
3
 import com.dmdirc.ktirc.IrcClient
4
 import com.dmdirc.ktirc.events.*
4
 import com.dmdirc.ktirc.events.*
5
 import com.dmdirc.ktirc.model.ChannelState
5
 import com.dmdirc.ktirc.model.ChannelState
6
+import com.dmdirc.ktirc.model.ChannelTopic
6
 import com.dmdirc.ktirc.model.ChannelUser
7
 import com.dmdirc.ktirc.model.ChannelUser
7
 import com.dmdirc.ktirc.util.logger
8
 import com.dmdirc.ktirc.util.logger
8
 
9
 
17
             is ChannelNamesReceived -> handleNamesReceived(client, event)
18
             is ChannelNamesReceived -> handleNamesReceived(client, event)
18
             is ChannelNamesFinished -> handleNamesFinished(client, event)
19
             is ChannelNamesFinished -> handleNamesFinished(client, event)
19
             is ChannelUserKicked -> handleKick(client, event)
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
             is ModeChanged -> handleModeChanged(client, event)
24
             is ModeChanged -> handleModeChanged(client, event)
21
             is UserQuit -> return handleQuit(client, event)
25
             is UserQuit -> return handleQuit(client, event)
22
             is UserNickChanged -> return handleNickChanged(client, event)
26
             is UserNickChanged -> return handleNickChanged(client, event)
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
     private fun handleModeChanged(client: IrcClient, event: ModeChanged) {
109
     private fun handleModeChanged(client: IrcClient, event: ModeChanged) {
80
         val chan = client.channelState[event.target] ?: return
110
         val chan = client.channelState[event.target] ?: return
81
         if (event.discovered) {
111
         if (event.discovered) {

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

8
 internal const val RPL_UMODEIS = "221"
8
 internal const val RPL_UMODEIS = "221"
9
 
9
 
10
 internal const val RPL_CHANNELMODEIS = "324"
10
 internal const val RPL_CHANNELMODEIS = "324"
11
+internal const val RPL_NOTOPIC = "331"
11
 internal const val RPL_TOPIC = "332"
12
 internal const val RPL_TOPIC = "332"
12
 internal const val RPL_TOPICWHOTIME = "333"
13
 internal const val RPL_TOPICWHOTIME = "333"
13
 internal const val RPL_MOTD = "372"
14
 internal const val RPL_MOTD = "372"

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

1
 package com.dmdirc.ktirc.messages
1
 package com.dmdirc.ktirc.messages
2
 
2
 
3
+import com.dmdirc.ktirc.events.ChannelTopicChanged
3
 import com.dmdirc.ktirc.events.ChannelTopicDiscovered
4
 import com.dmdirc.ktirc.events.ChannelTopicDiscovered
4
 import com.dmdirc.ktirc.events.ChannelTopicMetadataDiscovered
5
 import com.dmdirc.ktirc.events.ChannelTopicMetadataDiscovered
5
 import com.dmdirc.ktirc.model.IrcMessage
6
 import com.dmdirc.ktirc.model.IrcMessage
10
 
11
 
11
 internal class TopicProcessor : MessageProcessor {
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
     override fun process(message: IrcMessage) = sequence {
16
     override fun process(message: IrcMessage) = sequence {
16
         when (message.command) {
17
         when (message.command) {
17
             RPL_TOPIC -> yield(ChannelTopicDiscovered(message.time, message.channel, String(message.params[2])))
18
             RPL_TOPIC -> yield(ChannelTopicDiscovered(message.time, message.channel, String(message.params[2])))
19
+            RPL_NOTOPIC -> yield(ChannelTopicDiscovered(message.time, message.channel, null))
18
             RPL_TOPICWHOTIME -> yield(ChannelTopicMetadataDiscovered(
20
             RPL_TOPICWHOTIME -> yield(ChannelTopicMetadataDiscovered(
19
                     message.time, message.channel, message.params[2].asUser(), message.topicSetTime))
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
     }.toList()
24
     }.toList()
22
 
25
 

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

1
 package com.dmdirc.ktirc.model
1
 package com.dmdirc.ktirc.model
2
 
2
 
3
 import com.dmdirc.ktirc.io.CaseMapping
3
 import com.dmdirc.ktirc.io.CaseMapping
4
+import java.time.LocalDateTime
4
 
5
 
5
 /**
6
 /**
6
  * Describes the state of a channel that the client has joined.
7
  * Describes the state of a channel that the client has joined.
19
     var modesDiscovered = false
20
     var modesDiscovered = false
20
         internal set
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
      * A map of all users in the channel to their current modes.
35
      * A map of all users in the channel to their current modes.
24
      */
36
      */
34
     internal fun reset() {
46
     internal fun reset() {
35
         receivingUserList = false
47
         receivingUserList = false
36
         modesDiscovered = false
48
         modesDiscovered = false
49
+        topic = ChannelTopic()
50
+        topicDiscovered = false
37
         users.clear()
51
         users.clear()
38
         modes.clear()
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
  * Describes a user in a channel, and their modes.
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
 
5
 object TestConstants {
5
 object TestConstants {
6
     val time: LocalDateTime = LocalDateTime.parse("1995-09-15T09:00:00")
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
         assertEquals("bbb", channelStateMap["#thegibson"]?.modes?.get('b'))
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
 package com.dmdirc.ktirc.messages
1
 package com.dmdirc.ktirc.messages
2
 
2
 
3
 import com.dmdirc.ktirc.TestConstants
3
 import com.dmdirc.ktirc.TestConstants
4
+import com.dmdirc.ktirc.events.ChannelTopicChanged
4
 import com.dmdirc.ktirc.events.ChannelTopicDiscovered
5
 import com.dmdirc.ktirc.events.ChannelTopicDiscovered
5
 import com.dmdirc.ktirc.events.ChannelTopicMetadataDiscovered
6
 import com.dmdirc.ktirc.events.ChannelTopicMetadataDiscovered
6
 import com.dmdirc.ktirc.model.IrcMessage
7
 import com.dmdirc.ktirc.model.IrcMessage
8
+import com.dmdirc.ktirc.model.User
7
 import com.dmdirc.ktirc.params
9
 import com.dmdirc.ktirc.params
8
 import com.dmdirc.ktirc.util.currentTimeProvider
10
 import com.dmdirc.ktirc.util.currentTimeProvider
9
 import com.dmdirc.ktirc.util.currentTimeZoneProvider
11
 import com.dmdirc.ktirc.util.currentTimeZoneProvider
10
 import org.junit.jupiter.api.Assertions.assertEquals
12
 import org.junit.jupiter.api.Assertions.assertEquals
13
+import org.junit.jupiter.api.Assertions.assertNull
11
 import org.junit.jupiter.api.BeforeEach
14
 import org.junit.jupiter.api.BeforeEach
12
 import org.junit.jupiter.api.Test
15
 import org.junit.jupiter.api.Test
13
-import java.time.ZoneId
14
 
16
 
15
 internal class TopicProcessorTest {
17
 internal class TopicProcessorTest {
16
 
18
 
33
     }
35
     }
34
 
36
 
35
     @Test
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
         assertEquals(1, events.size)
51
         assertEquals(1, events.size)
39
 
52
 
40
         val event = events[0] as ChannelTopicMetadataDiscovered
53
         val event = events[0] as ChannelTopicMetadataDiscovered
41
         assertEquals(TestConstants.time, event.time)
54
         assertEquals(TestConstants.time, event.time)
42
         assertEquals("#thegibson", event.channel)
55
         assertEquals("#thegibson", event.channel)
43
         assertEquals("zeroCool", event.user.nickname)
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
     fun `reset resets all state`() = with(ChannelState("#thegibson") { CaseMapping.Rfc }) {
26
     fun `reset resets all state`() = with(ChannelState("#thegibson") { CaseMapping.Rfc }) {
27
         receivingUserList = true
27
         receivingUserList = true
28
         modesDiscovered = true
28
         modesDiscovered = true
29
+        topicDiscovered = true
29
         modes['a'] = "b"
30
         modes['a'] = "b"
30
         users += ChannelUser("acidBurn")
31
         users += ChannelUser("acidBurn")
32
+        topic = ChannelTopic("Hack the planet!")
31
 
33
 
32
         reset()
34
         reset()
33
 
35
 
34
         assertFalse(receivingUserList)
36
         assertFalse(receivingUserList)
35
         assertFalse(modesDiscovered)
37
         assertFalse(modesDiscovered)
38
+        assertFalse(topicDiscovered)
36
         assertTrue(modes.isEmpty())
39
         assertTrue(modes.isEmpty())
37
         assertEquals(0, users.count())
40
         assertEquals(0, users.count())
41
+        assertEquals(ChannelTopic(), topic)
38
     }
42
     }
39
 
43
 
40
 }
44
 }

Loading…
Cancel
Save