Browse Source

Support sending TAGMSGs and reactions

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

+ 3
- 1
CHANGELOG View File

@@ -3,11 +3,13 @@ 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
- * Message IDs and replies:
6
+ * Message extensions:
7 7
     * Added support for IRCv3 message tags v3.3
8 8
     * Exposed message IDs in MessageReceived and ActionReceived events
9 9
     * When sending a message you can now indicate what it is in reply to
10
+    * Added sendTagMessage() to send message tags without any content
10 11
     * The reply() utility automatically marks messages as a reply
12
+    * Added react() utility to send a reaction client tag
11 13
 
12 14
 v0.5.0
13 15
 

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

@@ -2,6 +2,8 @@ package com.dmdirc.ktirc.events
2 2
 
3 3
 import com.dmdirc.ktirc.IrcClient
4 4
 import com.dmdirc.ktirc.messages.sendMessage
5
+import com.dmdirc.ktirc.messages.sendTagMessage
6
+import com.dmdirc.ktirc.model.MessageTag
5 7
 import com.dmdirc.ktirc.model.ServerFeature
6 8
 import com.dmdirc.ktirc.model.asUser
7 9
 
@@ -25,9 +27,23 @@ internal fun ChannelNamesReceived.toModesAndUsers(client: IrcClient) = sequence
25 27
  * the message ID to tell other IRCv3 capable clients what message is being replied to.
26 28
  */
27 29
 fun IrcClient.reply(message: MessageReceived, response: String, prefixWithNickname: Boolean = false) {
28
-    if (caseMapping.areEquivalent(message.target, serverState.localNickname)) {
30
+    if (isToMe(message)) {
29 31
         sendMessage(message.user.nickname, response, message.messageId)
30 32
     } else {
31 33
         sendMessage(message.target, if (prefixWithNickname) "${message.user.nickname}: $response" else response, message.messageId)
32 34
     }
33
-}
35
+}
36
+
37
+/**
38
+ * "React" in the appropriate place to a message received.
39
+ */
40
+fun IrcClient.react(message: MessageReceived, reaction: String) = sendTagMessage(
41
+        if (isToMe(message)) message.user.nickname else message.target,
42
+        mapOf(MessageTag.React to reaction),
43
+        message.messageId)
44
+
45
+/**
46
+ * Utility to determine whether the given message is to our local user or not.
47
+ */
48
+internal fun IrcClient.isToMe(message: MessageReceived) =
49
+        caseMapping.areEquivalent(message.target, serverState.localNickname)

+ 30
- 5
src/main/kotlin/com/dmdirc/ktirc/messages/MessageBuilders.kt View File

@@ -33,14 +33,39 @@ fun IrcClient.sendAction(target: String, action: String) = sendCtcp(target, "ACT
33 33
 
34 34
 /** Sends a private message to a user or channel. */
35 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
36
+        sendWithTags(mapOf(MessageTag.Reply to inReplyTo), "PRIVMSG $target :$message")
37
+
38
+/**
39
+ * Sends a tag-only message.
40
+ *
41
+ * If [inReplyTo] is specified then the [MessageTag.Reply] tag will be automatically added.
42
+ */
43
+fun IrcClient.sendTagMessage(target: String, tags: Map<MessageTag, String>, inReplyTo: String? = null) {
44
+    sendWithTags(inReplyTo?.let { tags + (MessageTag.Reply to inReplyTo) } ?: tags, "TAGMSG $target")
45
+}
41 46
 
42 47
 /** Sends a message to register a user with the server. */
43 48
 internal fun IrcClient.sendUser(userName: String, realName: String) = send("USER $userName 0 * :$realName")
44 49
 
45 50
 /** Starts an authentication request. */
46 51
 internal fun IrcClient.sendAuthenticationMessage(data: String = "+") = send("AUTHENTICATE $data")
52
+
53
+/**
54
+ * Sends a message prefixed with some IRCv3 tags.
55
+ *
56
+ * For convenience, if the value of a tag is `null`, the tag will be omitted. If no tags are present the
57
+ * message is sent directly with no prefix.
58
+ */
59
+internal fun IrcClient.sendWithTags(tags: Map<MessageTag, String?>, message: String) = tags
60
+        .filterValues { it != null }
61
+        .map { (key, value) -> "${key.name}=${value?.escapeTagValue()}" }
62
+        .joinToString(";")
63
+        .let {
64
+            if (it.isEmpty()) send(message) else send("@$it $message")
65
+        }
66
+
67
+internal fun String.escapeTagValue() = replace("\\", "\\\\")
68
+        .replace("\n", "\\n")
69
+        .replace("\r", "\\r")
70
+        .replace(";", "\\:")
71
+        .replace(" ", "\\s")

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

@@ -38,6 +38,8 @@ sealed class MessageTag(val name: String) {
38 38
     object MessageId : MessageTag("draft/msgid")
39 39
     /** Used to identify a message ID that was replied to, to enable threaded conversations. */
40 40
     object Reply : MessageTag("+draft/reply")
41
+    /** Used to specify a slack-like reaction to another message. */
42
+    object React : MessageTag("+draft/react")
41 43
 }
42 44
 
43 45
 internal val messageTags: Map<String, MessageTag> by lazy {

+ 18
- 0
src/test/kotlin/com/dmdirc/ktirc/events/EventUtilsTest.kt View File

@@ -116,4 +116,22 @@ internal class EventUtilsTest {
116 116
         verify(ircClient).send("@+draft/reply=abc123 PRIVMSG #TheGibson :acidBurn: OK")
117 117
     }
118 118
 
119
+
120
+    @Test
121
+    fun `react sends response to user when message is private`() {
122
+        serverState.localNickname = "zeroCool"
123
+        val message = MessageReceived(TestConstants.time, User("acidBurn"), "Zerocool", "Hack the planet!", "msgId")
124
+
125
+        ircClient.react(message, ":P")
126
+        verify(ircClient).send("@+draft/react=:P;+draft/reply=msgId TAGMSG acidBurn")
127
+    }
128
+
129
+    @Test
130
+    fun `react sends unprefixed response to user when message is in a channel`() {
131
+        val message = MessageReceived(TestConstants.time, User("acidBurn"), "#TheGibson", "Hack the planet!", "msgId")
132
+
133
+        ircClient.react(message, ":P")
134
+        verify(ircClient).send("@+draft/react=:P;+draft/reply=msgId TAGMSG #TheGibson")
135
+    }
136
+
119 137
 }

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

@@ -1,8 +1,10 @@
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
 import com.nhaarman.mockitokotlin2.mock
5 6
 import com.nhaarman.mockitokotlin2.verify
7
+import org.junit.jupiter.api.Assertions.assertEquals
6 8
 import org.junit.jupiter.api.Test
7 9
 
8 10
 internal class MessageBuildersTest {
@@ -93,4 +95,45 @@ internal class MessageBuildersTest {
93 95
         verify(mockClient).send("AUTHENTICATE +")
94 96
     }
95 97
 
98
+    @Test
99
+    fun `sendWithTag sends message without tags`() {
100
+        mockClient.sendWithTags(emptyMap(), "PING")
101
+        verify(mockClient).send("PING")
102
+    }
103
+
104
+    @Test
105
+    fun `sendWithTag sends message with single tag`() {
106
+        mockClient.sendWithTags(mapOf(MessageTag.MessageId to "abc"), "PING")
107
+        verify(mockClient).send("@draft/msgid=abc PING")
108
+    }
109
+
110
+    @Test
111
+    fun `sendWithTag sends message with multiple tag`() {
112
+        mockClient.sendWithTags(mapOf(MessageTag.MessageId to "abc", MessageTag.AccountName to "foo"), "PING")
113
+        verify(mockClient).send("@draft/msgid=abc;account=foo PING")
114
+    }
115
+
116
+    @Test
117
+    fun `sendWithTag ignores tags with null values`() {
118
+        mockClient.sendWithTags(mapOf(MessageTag.MessageId to null, MessageTag.AccountName to "foo"), "PING")
119
+        verify(mockClient).send("@account=foo PING")
120
+    }
121
+
122
+    @Test
123
+    fun `sendTagMessage sends tags`() {
124
+        mockClient.sendTagMessage("#thegibson", mapOf(MessageTag.MessageId to "id", MessageTag.AccountName to "foo"))
125
+        verify(mockClient).send("@draft/msgid=id;account=foo TAGMSG #thegibson")
126
+    }
127
+
128
+    @Test
129
+    fun `sendTagMessage sends tags with reply ID`() {
130
+        mockClient.sendTagMessage("#thegibson", mapOf(MessageTag.MessageId to "id", MessageTag.AccountName to "foo"), "otherid")
131
+        verify(mockClient).send("@draft/msgid=id;account=foo;+draft/reply=otherid TAGMSG #thegibson")
132
+    }
133
+
134
+    @Test
135
+    fun `escapes tag values`() {
136
+        assertEquals("\\\\hack\\sthe\\r\\nplanet\\:", "\\hack the\r\nplanet;".escapeTagValue())
137
+    }
138
+
96 139
 }

Loading…
Cancel
Save