Browse Source

Support message tags v3.3, replies

tags/v0.6.0
Chris Smith 5 years ago
parent
commit
f67a441a08

+ 2
- 0
.gitignore View File

6
 /.gradle
6
 /.gradle
7
 /build
7
 /build
8
 /out
8
 /out
9
+# Temporary test files
10
+**/Test.kt

+ 6
- 0
CHANGELOG View File

2
 
2
 
3
  * Changed USER command to not send the server name, per modern standards
3
  * Changed USER command to not send the server name, per modern standards
4
  * Added support for SASL authentication (with PLAIN mechanism)
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
 v0.5.0
12
 v0.5.0
7
 
13
 

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

167
     private fun sendPasswordIfPresent() = server.password?.let(this::sendPassword)
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
  * Messages sent direct to the client will be responded to in direct message back; messages sent to a channel
20
  * Messages sent direct to the client will be responded to in direct message back; messages sent to a channel
21
  * will be replied to in the channel. If [prefixWithNickname] is `true`, channel messages will be prefixed
21
  * will be replied to in the channel. If [prefixWithNickname] is `true`, channel messages will be prefixed
22
  * with the other user's nickname (separated from the message by a colon and space).
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
 fun IrcClient.reply(message: MessageReceived, response: String, prefixWithNickname: Boolean = false) {
27
 fun IrcClient.reply(message: MessageReceived, response: String, prefixWithNickname: Boolean = false) {
25
     if (caseMapping.areEquivalent(message.target, serverState.localNickname)) {
28
     if (caseMapping.areEquivalent(message.target, serverState.localNickname)) {
26
-        sendMessage(message.user.nickname, response)
29
+        sendMessage(message.user.nickname, response, message.messageId)
27
     } else {
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
 class ChannelParted(time: LocalDateTime, val user: User, val channel: String, val reason: String = "") : IrcEvent(time)
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. */
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
 /** Raised when a user quits, and is in a channel. */
41
 /** Raised when a user quits, and is in a channel. */
42
 class ChannelQuit(time: LocalDateTime, val user: User, val channel: String, val reason: String = "") : IrcEvent(time)
42
 class ChannelQuit(time: LocalDateTime, val user: User, val channel: String, val reason: String = "") : IrcEvent(time)
48
 class ChannelNamesFinished(time: LocalDateTime, val channel: String) : IrcEvent(time)
48
 class ChannelNamesFinished(time: LocalDateTime, val channel: String) : IrcEvent(time)
49
 
49
 
50
 /** Raised when a message is received. */
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
  * Raised when a notice is received.
54
  * Raised when a notice is received.
58
 class NoticeReceived(time: LocalDateTime, val user: User, val target: String, val message: String) : IrcEvent(time)
58
 class NoticeReceived(time: LocalDateTime, val user: User, val target: String, val message: String) : IrcEvent(time)
59
 
59
 
60
 /** Raised when an action is received. */
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
 /** Raised when a CTCP is received. */
63
 /** Raised when a CTCP is received. */
64
 class CtcpReceived(time: LocalDateTime, val user: User, val target: String, val type: String, val content: String) : IrcEvent(time)
64
 class CtcpReceived(time: LocalDateTime, val user: User, val target: String, val type: String, val content: String) : IrcEvent(time)
74
  *
74
  *
75
  * This event is only raised if the server supports the `account-notify` capability.
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
 /** Raised when available server capabilities are received. More batches may follow. */
79
 /** Raised when available server capabilities are received. More batches may follow. */
80
 class ServerCapabilitiesReceived(time: LocalDateTime, val capabilities: Map<Capability, String>) : IrcEvent(time)
80
 class ServerCapabilitiesReceived(time: LocalDateTime, val capabilities: Map<Capability, String>) : IrcEvent(time)
86
 class ServerCapabilitiesFinished(time: LocalDateTime) : IrcEvent(time)
86
 class ServerCapabilitiesFinished(time: LocalDateTime) : IrcEvent(time)
87
 
87
 
88
 /** Raised when a Message Of the Day has completed. */
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
  * Raised when a mode change occurs.
92
  * Raised when a mode change occurs.
95
  * and the given modes are thus exhaustive. Otherwise, the modes are a sequence of changes to apply to the existing
95
  * and the given modes are thus exhaustive. Otherwise, the modes are a sequence of changes to apply to the existing
96
  * state.
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
 /** Raised when an AUTHENTICATION message is received. [argument] is `null` if the server sent an empty reply ("+") */
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
 /** Raised when a SASL attempt finishes, successfully or otherwise. */
103
 /** Raised when a SASL attempt finishes, successfully or otherwise. */
104
 class SaslFinished(time: LocalDateTime, var success: Boolean) : IrcEvent(time)
104
 class SaslFinished(time: LocalDateTime, var success: Boolean) : IrcEvent(time)

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

1
 package com.dmdirc.ktirc.messages
1
 package com.dmdirc.ktirc.messages
2
 
2
 
3
 import com.dmdirc.ktirc.IrcClient
3
 import com.dmdirc.ktirc.IrcClient
4
+import com.dmdirc.ktirc.model.MessageTag
4
 
5
 
5
 /** Sends a message to ask the server to list capabilities. */
6
 /** Sends a message to ask the server to list capabilities. */
6
 internal fun IrcClient.sendCapabilityList() = send("CAP LS 302")
7
 internal fun IrcClient.sendCapabilityList() = send("CAP LS 302")
31
 fun IrcClient.sendAction(target: String, action: String) = sendCtcp(target, "ACTION", action)
32
 fun IrcClient.sendAction(target: String, action: String) = sendCtcp(target, "ACTION", action)
32
 
33
 
33
 /** Sends a private message to a user or channel. */
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
 /** Sends a message to register a user with the server. */
42
 /** Sends a message to register a user with the server. */
37
 internal fun IrcClient.sendUser(userName: String, realName: String) = send("USER $userName 0 * :$realName")
43
 internal fun IrcClient.sendUser(userName: String, realName: String) = send("USER $userName 0 * :$realName")
38
 
44
 
39
 /** Starts an authentication request. */
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
 import com.dmdirc.ktirc.events.IrcEvent
5
 import com.dmdirc.ktirc.events.IrcEvent
6
 import com.dmdirc.ktirc.events.MessageReceived
6
 import com.dmdirc.ktirc.events.MessageReceived
7
 import com.dmdirc.ktirc.model.IrcMessage
7
 import com.dmdirc.ktirc.model.IrcMessage
8
+import com.dmdirc.ktirc.model.MessageTag
8
 import com.dmdirc.ktirc.model.User
9
 import com.dmdirc.ktirc.model.User
9
 
10
 
10
 internal class PrivmsgProcessor : MessageProcessor {
11
 internal class PrivmsgProcessor : MessageProcessor {
14
     override fun process(message: IrcMessage) = message.sourceUser?.let { user ->
15
     override fun process(message: IrcMessage) = message.sourceUser?.let { user ->
15
         listOf(when {
16
         listOf(when {
16
             message.isCtcp() -> handleCtcp(message, user)
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
     } ?: emptyList()
20
     } ?: emptyList()
20
 
21
 
23
         val parts = content.split(' ', limit=2)
24
         val parts = content.split(' ', limit=2)
24
         val body = if (parts.size == 2) parts[1] else ""
25
         val body = if (parts.size == 2) parts[1] else ""
25
         return when (parts[0].toUpperCase()) {
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
             else -> CtcpReceived(message.time, user, String(message.params[0]), parts[0], body)
28
             else -> CtcpReceived(message.time, user, String(message.params[0]), parts[0], body)
28
         }
29
         }
29
     }
30
     }
30
 
31
 
31
     private fun IrcMessage.isCtcp() = params[1].size > 2 && params[1][0] == CTCP_BYTE && params[1][params[1].size - 1] == CTCP_BYTE
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
     object SaslAuthentication : Capability("sasl")
54
     object SaslAuthentication : Capability("sasl")
55
 
55
 
56
     // Capabilities that enable more information in message tags:
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
     /** Messages are tagged with the server time they originated at. */
60
     /** Messages are tagged with the server time they originated at. */
58
     object ServerTimeMessageTag : Capability("server-time")
61
     object ServerTimeMessageTag : Capability("server-time")
59
 
62
 
76
 
79
 
77
     // Capabilities that notify us of changes to other clients:
80
     // Capabilities that notify us of changes to other clients:
78
     /** Receive a notification when a user's account changes. */
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
     /** Receive a notification when a user's away state changes. */
84
     /** Receive a notification when a user's away state changes. */
82
     object AwayStateMessages : Capability("away-notify") // TODO: Add processor
85
     object AwayStateMessages : Capability("away-notify") // TODO: Add processor

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

34
     object AccountName : MessageTag("account")
34
     object AccountName : MessageTag("account")
35
     /** Specifies the time the server received the message, if the `server-time` capability is negotiated. */
35
     /** Specifies the time the server received the message, if the `server-time` capability is negotiated. */
36
     object ServerTime : MessageTag("time")
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
 internal val messageTags: Map<String, MessageTag> by lazy {
43
 internal val messageTags: Map<String, MessageTag> by lazy {

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

91
         verify(ircClient).send("PRIVMSG #TheGibson :acidBurn: OK")
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
         verify(mockClient).send("PRIVMSG acidBurn :Hack the planet!")
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
     @Test
60
     @Test
55
     fun `sendCtcp sends correct CTCP message with no arguments`() {
61
     fun `sendCtcp sends correct CTCP message with no arguments`() {
56
         mockClient.sendCtcp("acidBurn", "ping")
62
         mockClient.sendCtcp("acidBurn", "ping")

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

5
 import com.dmdirc.ktirc.events.CtcpReceived
5
 import com.dmdirc.ktirc.events.CtcpReceived
6
 import com.dmdirc.ktirc.events.MessageReceived
6
 import com.dmdirc.ktirc.events.MessageReceived
7
 import com.dmdirc.ktirc.model.IrcMessage
7
 import com.dmdirc.ktirc.model.IrcMessage
8
+import com.dmdirc.ktirc.model.MessageTag
8
 import com.dmdirc.ktirc.model.User
9
 import com.dmdirc.ktirc.model.User
9
 import com.dmdirc.ktirc.params
10
 import com.dmdirc.ktirc.params
10
 import com.dmdirc.ktirc.util.currentTimeProvider
11
 import com.dmdirc.ktirc.util.currentTimeProvider
11
 import org.junit.jupiter.api.Assertions.assertEquals
12
 import org.junit.jupiter.api.Assertions.assertEquals
13
+import org.junit.jupiter.api.Assertions.assertNull
12
 import org.junit.jupiter.api.BeforeEach
14
 import org.junit.jupiter.api.BeforeEach
13
 import org.junit.jupiter.api.Test
15
 import org.junit.jupiter.api.Test
14
 
16
 
30
         assertEquals(User("acidburn", "libby", "root.localhost"), event.user)
32
         assertEquals(User("acidburn", "libby", "root.localhost"), event.user)
31
         assertEquals("#crashandburn", event.target)
33
         assertEquals("#crashandburn", event.target)
32
         assertEquals("hack the planet!", event.message)
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
     @Test
52
     @Test
43
         assertEquals(User("acidburn", "libby", "root.localhost"), event.user)
60
         assertEquals(User("acidburn", "libby", "root.localhost"), event.user)
44
         assertEquals("#crashandburn", event.target)
61
         assertEquals("#crashandburn", event.target)
45
         assertEquals("hacks the planet", event.action)
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
     @Test
80
     @Test
56
         assertEquals(User("acidburn", "libby", "root.localhost"), event.user)
88
         assertEquals(User("acidburn", "libby", "root.localhost"), event.user)
57
         assertEquals("#crashandburn", event.target)
89
         assertEquals("#crashandburn", event.target)
58
         assertEquals("", event.action)
90
         assertEquals("", event.action)
91
+        assertNull(event.messageId)
59
     }
92
     }
60
 
93
 
61
     @Test
94
     @Test
69
         assertEquals(User("acidburn", "libby", "root.localhost"), event.user)
102
         assertEquals(User("acidburn", "libby", "root.localhost"), event.user)
70
         assertEquals("#crashandburn", event.target)
103
         assertEquals("#crashandburn", event.target)
71
         assertEquals("hacks the planet", event.action)
104
         assertEquals("hacks the planet", event.action)
105
+        assertNull(event.messageId)
72
     }
106
     }
73
 
107
 
74
     @Test
108
     @Test

Loading…
Cancel
Save