Browse Source

Support message tags v3.3, replies

tags/v0.6.0
Chris Smith 6 months ago
parent
commit
f67a441a08

+ 2
- 0
.gitignore View File

@@ -6,3 +6,5 @@
6 6
 /.gradle
7 7
 /build
8 8
 /out
9
+# Temporary test files
10
+**/Test.kt

+ 6
- 0
CHANGELOG View File

@@ -2,6 +2,12 @@ vNEXT (in development)
2 2
 
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
+ * Removed some unused test code
6
+ * Message IDs and replies:
7
+    * Added support for IRCv3 message tags v3.3
8
+    * Exposed message IDs in MessageReceived and ActionReceived events
9
+    * When sending a message you can now indicate what it is in reply to
10
+    * The reply() utility automatically marks messages as a reply
5 11
 
6 12
 v0.5.0
7 13
 

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

@@ -167,26 +167,3 @@ class IrcClientImpl(private val server: Server, override val profile: Profile) :
167 167
     private fun sendPasswordIfPresent() = server.password?.let(this::sendPassword)
168 168
 
169 169
 }
170
-
171
-internal fun main() {
172
-    val rootLogger = LogManager.getLogManager().getLogger("")
173
-    rootLogger.level = Level.FINEST
174
-    for (h in rootLogger.handlers) {
175
-        h.level = Level.FINEST
176
-    }
177
-
178
-    runBlocking {
179
-        with(IrcClientImpl(Server("testnet.inspircd.org", 6667), Profile("KtIrc", "Kotlin!", "kotlin"))) {
180
-            onEvent { event ->
181
-                when (event) {
182
-                    is ServerWelcome -> sendJoin("#ktirc")
183
-                    is MessageReceived ->
184
-                        if (event.message == "!test")
185
-                            reply(event, "Test successful!")
186
-                }
187
-            }
188
-            connect()
189
-            join()
190
-        }
191
-    }
192
-}

+ 5
- 2
src/main/kotlin/com/dmdirc/ktirc/events/EventUtils.kt View File

@@ -20,11 +20,14 @@ internal fun ChannelNamesReceived.toModesAndUsers(client: IrcClient) = sequence
20 20
  * Messages sent direct to the client will be responded to in direct message back; messages sent to a channel
21 21
  * will be replied to in the channel. If [prefixWithNickname] is `true`, channel messages will be prefixed
22 22
  * with the other user's nickname (separated from the message by a colon and space).
23
+ *
24
+ * If the given [message] has a [com.dmdirc.ktirc.model.MessageTag.MessageId] tag then the reply will include
25
+ * the message ID to tell other IRCv3 capable clients what message is being replied to.
23 26
  */
24 27
 fun IrcClient.reply(message: MessageReceived, response: String, prefixWithNickname: Boolean = false) {
25 28
     if (caseMapping.areEquivalent(message.target, serverState.localNickname)) {
26
-        sendMessage(message.user.nickname, response)
29
+        sendMessage(message.user.nickname, response, message.messageId)
27 30
     } else {
28
-        sendMessage(message.target, if (prefixWithNickname) "${message.user.nickname}: $response" else response)
31
+        sendMessage(message.target, if (prefixWithNickname) "${message.user.nickname}: $response" else response, message.messageId)
29 32
     }
30 33
 }

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

@@ -36,7 +36,7 @@ class ChannelJoined(time: LocalDateTime, val user: User, val channel: String) :
36 36
 class ChannelParted(time: LocalDateTime, val user: User, val channel: String, val reason: String = "") : IrcEvent(time)
37 37
 
38 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)
39
+class ChannelUserKicked(time: LocalDateTime, val user: User, val channel: String, val victim: String, val reason: String = "") : IrcEvent(time)
40 40
 
41 41
 /** Raised when a user quits, and is in a channel. */
42 42
 class ChannelQuit(time: LocalDateTime, val user: User, val channel: String, val reason: String = "") : IrcEvent(time)
@@ -48,7 +48,7 @@ class ChannelNamesReceived(time: LocalDateTime, val channel: String, val names:
48 48
 class ChannelNamesFinished(time: LocalDateTime, val channel: String) : IrcEvent(time)
49 49
 
50 50
 /** Raised when a message is received. */
51
-class MessageReceived(time: LocalDateTime, val user: User, val target: String, val message: String) : IrcEvent(time)
51
+class MessageReceived(time: LocalDateTime, val user: User, val target: String, val message: String, val messageId: String? = null) : IrcEvent(time)
52 52
 
53 53
 /**
54 54
  * Raised when a notice is received.
@@ -58,7 +58,7 @@ class MessageReceived(time: LocalDateTime, val user: User, val target: String, v
58 58
 class NoticeReceived(time: LocalDateTime, val user: User, val target: String, val message: String) : IrcEvent(time)
59 59
 
60 60
 /** Raised when an action is received. */
61
-class ActionReceived(time: LocalDateTime, val user: User, val target: String, val action: String) : IrcEvent(time)
61
+class ActionReceived(time: LocalDateTime, val user: User, val target: String, val action: String, val messageId: String? = null) : IrcEvent(time)
62 62
 
63 63
 /** Raised when a CTCP is received. */
64 64
 class CtcpReceived(time: LocalDateTime, val user: User, val target: String, val type: String, val content: String) : IrcEvent(time)
@@ -74,7 +74,7 @@ class UserQuit(time: LocalDateTime, val user: User, val reason: String = "") : I
74 74
  *
75 75
  * This event is only raised if the server supports the `account-notify` capability.
76 76
  */
77
-class UserAccountChanged(time: LocalDateTime, val user: User, val newAccount: String?): IrcEvent(time)
77
+class UserAccountChanged(time: LocalDateTime, val user: User, val newAccount: String?) : IrcEvent(time)
78 78
 
79 79
 /** Raised when available server capabilities are received. More batches may follow. */
80 80
 class ServerCapabilitiesReceived(time: LocalDateTime, val capabilities: Map<Capability, String>) : IrcEvent(time)
@@ -86,7 +86,7 @@ class ServerCapabilitiesAcknowledged(time: LocalDateTime, val capabilities: Map<
86 86
 class ServerCapabilitiesFinished(time: LocalDateTime) : IrcEvent(time)
87 87
 
88 88
 /** Raised when a Message Of the Day has completed. */
89
-class MotdFinished(time: LocalDateTime, val missing: Boolean = false): IrcEvent(time)
89
+class MotdFinished(time: LocalDateTime, val missing: Boolean = false) : IrcEvent(time)
90 90
 
91 91
 /**
92 92
  * Raised when a mode change occurs.
@@ -95,10 +95,10 @@ class MotdFinished(time: LocalDateTime, val missing: Boolean = false): IrcEvent(
95 95
  * and the given modes are thus exhaustive. Otherwise, the modes are a sequence of changes to apply to the existing
96 96
  * state.
97 97
  */
98
-class ModeChanged(time: LocalDateTime, val target: String, val modes: String, val arguments: Array<String>, val discovered: Boolean = false): IrcEvent(time)
98
+class ModeChanged(time: LocalDateTime, val target: String, val modes: String, val arguments: Array<String>, val discovered: Boolean = false) : IrcEvent(time)
99 99
 
100 100
 /** Raised when an AUTHENTICATION message is received. [argument] is `null` if the server sent an empty reply ("+") */
101
-class AuthenticationMessage(time: LocalDateTime, val argument: String?): IrcEvent(time)
101
+class AuthenticationMessage(time: LocalDateTime, val argument: String?) : IrcEvent(time)
102 102
 
103 103
 /** Raised when a SASL attempt finishes, successfully or otherwise. */
104 104
 class SaslFinished(time: LocalDateTime, var success: Boolean) : IrcEvent(time)

+ 8
- 2
src/main/kotlin/com/dmdirc/ktirc/messages/MessageBuilders.kt View File

@@ -1,6 +1,7 @@
1 1
 package com.dmdirc.ktirc.messages
2 2
 
3 3
 import com.dmdirc.ktirc.IrcClient
4
+import com.dmdirc.ktirc.model.MessageTag
4 5
 
5 6
 /** Sends a message to ask the server to list capabilities. */
6 7
 internal fun IrcClient.sendCapabilityList() = send("CAP LS 302")
@@ -31,10 +32,15 @@ fun IrcClient.sendCtcp(target: String, type: String, data: String? = null) =
31 32
 fun IrcClient.sendAction(target: String, action: String) = sendCtcp(target, "ACTION", action)
32 33
 
33 34
 /** Sends a private message to a user or channel. */
34
-fun IrcClient.sendMessage(target: String, message: String) = send("PRIVMSG $target :$message")
35
+fun IrcClient.sendMessage(target: String, message: String, inReplyTo: String? = null) =
36
+        if (inReplyTo == null)
37
+            send("PRIVMSG $target :$message")
38
+        else
39
+            send("@${MessageTag.Reply.name}=$inReplyTo PRIVMSG $target :$message")
40
+            // TODO ^ Proper tag building/serializing
35 41
 
36 42
 /** Sends a message to register a user with the server. */
37 43
 internal fun IrcClient.sendUser(userName: String, realName: String) = send("USER $userName 0 * :$realName")
38 44
 
39 45
 /** Starts an authentication request. */
40
-internal fun IrcClient.sendAuthenticationMessage(data: String = "+") =send("AUTHENTICATE $data")
46
+internal fun IrcClient.sendAuthenticationMessage(data: String = "+") = send("AUTHENTICATE $data")

+ 6
- 2
src/main/kotlin/com/dmdirc/ktirc/messages/PrivmsgProcessor.kt View File

@@ -5,6 +5,7 @@ import com.dmdirc.ktirc.events.CtcpReceived
5 5
 import com.dmdirc.ktirc.events.IrcEvent
6 6
 import com.dmdirc.ktirc.events.MessageReceived
7 7
 import com.dmdirc.ktirc.model.IrcMessage
8
+import com.dmdirc.ktirc.model.MessageTag
8 9
 import com.dmdirc.ktirc.model.User
9 10
 
10 11
 internal class PrivmsgProcessor : MessageProcessor {
@@ -14,7 +15,7 @@ internal class PrivmsgProcessor : MessageProcessor {
14 15
     override fun process(message: IrcMessage) = message.sourceUser?.let { user ->
15 16
         listOf(when {
16 17
             message.isCtcp() -> handleCtcp(message, user)
17
-            else -> MessageReceived(message.time, user, String(message.params[0]), String(message.params[1]))
18
+            else -> MessageReceived(message.time, user, String(message.params[0]), String(message.params[1]), message.messageId)
18 19
         })
19 20
     } ?: emptyList()
20 21
 
@@ -23,11 +24,14 @@ internal class PrivmsgProcessor : MessageProcessor {
23 24
         val parts = content.split(' ', limit=2)
24 25
         val body = if (parts.size == 2) parts[1] else ""
25 26
         return when (parts[0].toUpperCase()) {
26
-            "ACTION" -> ActionReceived(message.time, user, String(message.params[0]), body)
27
+            "ACTION" -> ActionReceived(message.time, user, String(message.params[0]), body, message.messageId)
27 28
             else -> CtcpReceived(message.time, user, String(message.params[0]), parts[0], body)
28 29
         }
29 30
     }
30 31
 
31 32
     private fun IrcMessage.isCtcp() = params[1].size > 2 && params[1][0] == CTCP_BYTE && params[1][params[1].size - 1] == CTCP_BYTE
32 33
 
34
+    private val IrcMessage.messageId
35
+        get() = tags[MessageTag.MessageId]
36
+
33 37
 }

+ 4
- 1
src/main/kotlin/com/dmdirc/ktirc/model/CapabilitiesState.kt View File

@@ -54,6 +54,9 @@ sealed class Capability(val name: String) {
54 54
     object SaslAuthentication : Capability("sasl")
55 55
 
56 56
     // Capabilities that enable more information in message tags:
57
+    /** Draft version of message tags, enables client-only tags. */
58
+    object DraftMessageTags33 : Capability("draft/message-tags-0.2") // TODO: Add processor for TAGMSG
59
+
57 60
     /** Messages are tagged with the server time they originated at. */
58 61
     object ServerTimeMessageTag : Capability("server-time")
59 62
 
@@ -76,7 +79,7 @@ sealed class Capability(val name: String) {
76 79
 
77 80
     // Capabilities that notify us of changes to other clients:
78 81
     /** Receive a notification when a user's account changes. */
79
-    object AccountChangeMessages : Capability("account-notify") // TODO: Add processor
82
+    object AccountChangeMessages : Capability("account-notify")
80 83
 
81 84
     /** Receive a notification when a user's away state changes. */
82 85
     object AwayStateMessages : Capability("away-notify") // TODO: Add processor

+ 4
- 0
src/main/kotlin/com/dmdirc/ktirc/model/IrcMessage.kt View File

@@ -34,6 +34,10 @@ sealed class MessageTag(val name: String) {
34 34
     object AccountName : MessageTag("account")
35 35
     /** Specifies the time the server received the message, if the `server-time` capability is negotiated. */
36 36
     object ServerTime : MessageTag("time")
37
+    /** A unique ID for the message, used to reply, react, edit, delete, etc. */
38
+    object MessageId : MessageTag("draft/msgid")
39
+    /** Used to identify a message ID that was replied to, to enable threaded conversations. */
40
+    object Reply : MessageTag("+draft/reply")
37 41
 }
38 42
 
39 43
 internal val messageTags: Map<String, MessageTag> by lazy {

+ 25
- 1
src/test/kotlin/com/dmdirc/ktirc/events/EventUtilsTest.kt View File

@@ -91,5 +91,29 @@ internal class EventUtilsTest {
91 91
         verify(ircClient).send("PRIVMSG #TheGibson :acidBurn: OK")
92 92
     }
93 93
 
94
+    @Test
95
+    fun `reply sends response with message ID to user when message is private`() {
96
+        serverState.localNickname = "zeroCool"
97
+        val message = MessageReceived(TestConstants.time, User("acidBurn"), "Zerocool", "Hack the planet!", "abc123")
98
+
99
+        ircClient.reply(message, "OK")
100
+        verify(ircClient).send("@+draft/reply=abc123 PRIVMSG acidBurn :OK")
101
+    }
102
+
103
+    @Test
104
+    fun `reply sends unprefixed response with message ID to user when message is in a channel`() {
105
+        val message = MessageReceived(TestConstants.time, User("acidBurn"), "#TheGibson", "Hack the planet!", "abc123")
106
+
107
+        ircClient.reply(message, "OK")
108
+        verify(ircClient).send("@+draft/reply=abc123 PRIVMSG #TheGibson :OK")
109
+    }
110
+
111
+    @Test
112
+    fun `reply sends prefixed response with message ID to user when message is in a channel`() {
113
+        val message = MessageReceived(TestConstants.time, User("acidBurn"), "#TheGibson", "Hack the planet!", "abc123")
114
+
115
+        ircClient.reply(message, "OK", prefixWithNickname = true)
116
+        verify(ircClient).send("@+draft/reply=abc123 PRIVMSG #TheGibson :acidBurn: OK")
117
+    }
94 118
 
95
-}
119
+}

+ 6
- 0
src/test/kotlin/com/dmdirc/ktirc/messages/MessageBuildersTest.kt View File

@@ -51,6 +51,12 @@ internal class MessageBuildersTest {
51 51
         verify(mockClient).send("PRIVMSG acidBurn :Hack the planet!")
52 52
     }
53 53
 
54
+    @Test
55
+    fun `sendMessage sends correct PRIVMSG message with reply to tag`() {
56
+        mockClient.sendMessage("acidBurn", "Hack the planet!", "abc123")
57
+        verify(mockClient).send("@+draft/reply=abc123 PRIVMSG acidBurn :Hack the planet!")
58
+    }
59
+
54 60
     @Test
55 61
     fun `sendCtcp sends correct CTCP message with no arguments`() {
56 62
         mockClient.sendCtcp("acidBurn", "ping")

+ 34
- 0
src/test/kotlin/com/dmdirc/ktirc/messages/PrivmsgProcessorTest.kt View File

@@ -5,10 +5,12 @@ import com.dmdirc.ktirc.events.ActionReceived
5 5
 import com.dmdirc.ktirc.events.CtcpReceived
6 6
 import com.dmdirc.ktirc.events.MessageReceived
7 7
 import com.dmdirc.ktirc.model.IrcMessage
8
+import com.dmdirc.ktirc.model.MessageTag
8 9
 import com.dmdirc.ktirc.model.User
9 10
 import com.dmdirc.ktirc.params
10 11
 import com.dmdirc.ktirc.util.currentTimeProvider
11 12
 import org.junit.jupiter.api.Assertions.assertEquals
13
+import org.junit.jupiter.api.Assertions.assertNull
12 14
 import org.junit.jupiter.api.BeforeEach
13 15
 import org.junit.jupiter.api.Test
14 16
 
@@ -30,6 +32,21 @@ internal class PrivmsgProcessorTest {
30 32
         assertEquals(User("acidburn", "libby", "root.localhost"), event.user)
31 33
         assertEquals("#crashandburn", event.target)
32 34
         assertEquals("hack the planet!", event.message)
35
+        assertNull(event.messageId)
36
+    }
37
+
38
+    @Test
39
+    fun `PrivsgProcessor raises message received event with message ID`() {
40
+        val events = PrivmsgProcessor().process(
41
+                IrcMessage(mapOf(MessageTag.MessageId to "abc123"), "acidburn!libby@root.localhost".toByteArray(), "PRIVMSG", params("#crashandburn", "hack the planet!")))
42
+        assertEquals(1, events.size)
43
+
44
+        val event = events[0] as MessageReceived
45
+        assertEquals(TestConstants.time, event.time)
46
+        assertEquals(User("acidburn", "libby", "root.localhost"), event.user)
47
+        assertEquals("#crashandburn", event.target)
48
+        assertEquals("hack the planet!", event.message)
49
+        assertEquals("abc123", event.messageId)
33 50
     }
34 51
 
35 52
     @Test
@@ -43,6 +60,21 @@ internal class PrivmsgProcessorTest {
43 60
         assertEquals(User("acidburn", "libby", "root.localhost"), event.user)
44 61
         assertEquals("#crashandburn", event.target)
45 62
         assertEquals("hacks the planet", event.action)
63
+        assertNull(event.messageId)
64
+    }
65
+
66
+    @Test
67
+    fun `PrivsgProcessor raises action received event with message ID`() {
68
+        val events = PrivmsgProcessor().process(
69
+                IrcMessage(mapOf(MessageTag.MessageId to "abc123"), "acidburn!libby@root.localhost".toByteArray(), "PRIVMSG", params("#crashandburn", "\u0001ACTION hacks the planet\u0001")))
70
+        assertEquals(1, events.size)
71
+
72
+        val event = events[0] as ActionReceived
73
+        assertEquals(TestConstants.time, event.time)
74
+        assertEquals(User("acidburn", "libby", "root.localhost"), event.user)
75
+        assertEquals("#crashandburn", event.target)
76
+        assertEquals("hacks the planet", event.action)
77
+        assertEquals("abc123", event.messageId)
46 78
     }
47 79
 
48 80
     @Test
@@ -56,6 +88,7 @@ internal class PrivmsgProcessorTest {
56 88
         assertEquals(User("acidburn", "libby", "root.localhost"), event.user)
57 89
         assertEquals("#crashandburn", event.target)
58 90
         assertEquals("", event.action)
91
+        assertNull(event.messageId)
59 92
     }
60 93
 
61 94
     @Test
@@ -69,6 +102,7 @@ internal class PrivmsgProcessorTest {
69 102
         assertEquals(User("acidburn", "libby", "root.localhost"), event.user)
70 103
         assertEquals("#crashandburn", event.target)
71 104
         assertEquals("hacks the planet", event.action)
105
+        assertNull(event.messageId)
72 106
     }
73 107
 
74 108
     @Test

Loading…
Cancel
Save