borogove
created pr with
borogove/multi-dm.1
added borogove/multi-dm.2
1: 0ef2769 = 1: 41e89c8 Use to for chatId if not in recipients
2: 90e5a6a = 2: c323e3d clone should clone attributes
3: d86ea28 = 3: a1199ae Actually send to all recipients
4: 1c1061f ! 4: 94bf87d Never add self as counterpart
5: 9c8aa41 = 5: 32b9fad Extended addressing for sent messages specifies all recipients
6: 2da9124 = 6: ce4d9e1 Always add all recipients to outbound message
7: e7db006 = 7: 350343e Use a shared helper for adding extended addressing
8: 4530ece = 8: 40f662a Add startMultiChat to create multiDMs
9: 6810ba9 = 9: 8043c53 Never include ourselves in incoming chat ids
-: ------- > 10: 68e7246 Cannot MAM or bookmark a multiDM
cmds
checkout latest patchset:
ssh pr.pico.sh pull borogove:multi-dm | git am -3checkout specific patchset revision:
ssh pr.pico.sh pull borogove:multi-dm [rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh borogove:multi-dmadd comment to patch request:
ssh pr.pico.sh comment borogove:multi-dm "lgtm!"
Patchset
borogove/multi-dm.1
Use to for chatId if not in recipients
Stephen Paul Weber
clone should clone attributes
2026-11-08T19:15:14ZStephen Paul Weber
Actually send to all recipients
2026-11-09T16:18:06ZStephen Paul Weber
Never add self as counterpart
2026-11-09T16:24:50ZStephen Paul Weber
→ Extended addressing for sent messages specifies all recipients
2026-11-08T19:20:04ZStephen Paul Weber
Always add all recipients to outbound message
2026-11-08T19:20:46ZStephen Paul Weber
Use a shared helper for adding extended addressing
2026-11-08T19:21:50ZStephen Paul Weber
Add startMultiChat to create multiDMs
2026-11-08T19:32:30ZStephen Paul Weber
Never include ourselves in incoming chat ids
2026-11-08T21:01:35ZStephen Paul Weber
2026-11-09T19:40:59Z
Extended addressing for sent messages specifies all recipients
Stephen Paul Weber
2026-11-08T19:20:46ZDo not also include "to" it might be eg 1,2,3@cheogram.com
Semantic diff summary
0 added,
1 modified,
0 signature changed,
0 removed
across 1 analyzed file
(4 files skipped: unsupported file type)
+5
-0
borogove/Message.hx
#
| ... | ... | @@ -179,6 +179,11 @@ class Message { | |
| 179 | 179 | final addresses = stanza.getChild("addresses", "http://jabber.org/protocol/address"); | |
| 180 | 180 | var anyExtendedReplyTo = false; | |
| 181 | 181 | if (addresses != null) { | |
| 182 | + | if (msg.direction == MessageSent) { | |
| 183 | + | if (addresses.allTags("address").exists((address) -> address.attr.get("type") == "to")) { | |
| 184 | + | recipients.clear(); | |
| 185 | + | } | |
| 186 | + | } | |
| 182 | 187 | for (address in addresses.allTags("address")) { | |
| 183 | 188 | final jid = address.attr.get("jid"); | |
| 184 | 189 | if (address.attr.get("type") == "noreply") { |
+142
-0
test/TestChatMessage.hx
#
| ... | ... | @@ -221,6 +221,148 @@ class TestChatMessage extends utest.Test { | |
| 221 | 221 | Assert.equals("> line one\n> line two\n> line three\n\nok sure", body); | |
| 222 | 222 | } | |
| 223 | 223 | ||
| 224 | + | public function testIncomingExtendedAddressingIncludesSelfInRecipients() { | |
| 225 | + | final stanza = new Stanza("message"); | |
| 226 | + | stanza.attr.set("type", "chat"); | |
| 227 | + | stanza.attr.set("id", "extended-in-1"); | |
| 228 | + | stanza.attr.set("from", "alice@example.com"); | |
| 229 | + | stanza.attr.set("to", "me@example.com"); | |
| 230 | + | final addresses = stanza.tag("addresses", {xmlns: "http://jabber.org/protocol/address"}); | |
| 231 | + | addresses.tag("address", {type: "to", jid: "alice@example.com"}).up(); | |
| 232 | + | addresses.tag("address", {type: "to", jid: "bob@example.com"}).up(); | |
| 233 | + | addresses.up(); | |
| 234 | + | stanza.addChild(new Stanza("body").text("hi")); | |
| 235 | + | ||
| 236 | + | final msg = Message.fromStanza(stanza, JID.parse("me@example.com")); | |
| 237 | + | switch (msg.parsed) { | |
| 238 | + | case ChatMessageStanza(m): | |
| 239 | + | Assert.equals("alice@example.com\nbob@example.com", m.chatId()); | |
| 240 | + | Assert.equals("me@example.com", m.to?.asString()); | |
| 241 | + | Assert.same(["alice@example.com", "bob@example.com", "me@example.com"], m.recipients.map(r -> r.asBare().asString())); | |
| 242 | + | default: | |
| 243 | + | Assert.fail("Expected ChatMessageStanza"); | |
| 244 | + | } | |
| 245 | + | } | |
| 246 | + | ||
| 247 | + | public function testIncomingAddressingDoesNotDuplicateSelfInRecipients() { | |
| 248 | + | final stanza = new Stanza("message"); | |
| 249 | + | stanza.attr.set("type", "chat"); | |
| 250 | + | stanza.attr.set("id", "extended-in-2"); | |
| 251 | + | stanza.attr.set("from", "alice@example.com"); | |
| 252 | + | stanza.attr.set("to", "me@example.com"); | |
| 253 | + | final addresses = stanza.tag("addresses", {xmlns: "http://jabber.org/protocol/address"}); | |
| 254 | + | addresses.tag("address", {type: "to", jid: "alice@example.com"}).up(); | |
| 255 | + | addresses.tag("address", {type: "to", jid: "me@example.com"}).up(); | |
| 256 | + | addresses.up(); | |
| 257 | + | stanza.addChild(new Stanza("body").text("hi")); | |
| 258 | + | ||
| 259 | + | final msg = Message.fromStanza(stanza, JID.parse("me@example.com")); | |
| 260 | + | switch (msg.parsed) { | |
| 261 | + | case ChatMessageStanza(m): | |
| 262 | + | Assert.same(["alice@example.com", "me@example.com"], m.recipients.map(r -> r.asBare().asString())); | |
| 263 | + | default: | |
| 264 | + | Assert.fail("Expected ChatMessageStanza"); | |
| 265 | + | } | |
| 266 | + | } | |
| 267 | + | ||
| 268 | + | public function testSentNonRecipientAddressingKeepsEnvelopeToInRecipients() { | |
| 269 | + | final stanza = new Stanza("message"); | |
| 270 | + | stanza.attr.set("type", "chat"); | |
| 271 | + | stanza.attr.set("id", "extended-out-1"); | |
| 272 | + | stanza.attr.set("from", "me@example.com"); | |
| 273 | + | stanza.attr.set("to", "bob@example.com"); | |
| 274 | + | final addresses = stanza.tag("addresses", {xmlns: "http://jabber.org/protocol/address"}); | |
| 275 | + | addresses.tag("address", {type: "replyto", jid: "multi@example.com"}).up(); | |
| 276 | + | addresses.up(); | |
| 277 | + | stanza.addChild(new Stanza("body").text("hi")); | |
| 278 | + | ||
| 279 | + | final msg = Message.fromStanza(stanza, JID.parse("me@example.com")); | |
| 280 | + | switch (msg.parsed) { | |
| 281 | + | case ChatMessageStanza(m): | |
| 282 | + | Assert.equals("bob@example.com", m.chatId()); | |
| 283 | + | Assert.same(["bob@example.com"], m.recipients.map(r -> r.asBare().asString())); | |
| 284 | + | Assert.same(["multi@example.com"], m.replyTo.map(r -> r.asBare().asString())); | |
| 285 | + | default: | |
| 286 | + | Assert.fail("Expected ChatMessageStanza"); | |
| 287 | + | } | |
| 288 | + | } | |
| 289 | + | ||
| 290 | + | public function testSentCcAddressingDoesNotDropEnvelopeTo() { | |
| 291 | + | final stanza = new Stanza("message"); | |
| 292 | + | stanza.attr.set("type", "chat"); | |
| 293 | + | stanza.attr.set("id", "extended-out-2"); | |
| 294 | + | stanza.attr.set("from", "me@example.com"); | |
| 295 | + | stanza.attr.set("to", "bob@example.com"); | |
| 296 | + | final addresses = stanza.tag("addresses", {xmlns: "http://jabber.org/protocol/address"}); | |
| 297 | + | addresses.tag("address", {type: "cc", jid: "carol@example.com"}).up(); | |
| 298 | + | addresses.up(); | |
| 299 | + | stanza.addChild(new Stanza("body").text("hi")); | |
| 300 | + | ||
| 301 | + | final msg = Message.fromStanza(stanza, JID.parse("me@example.com")); | |
| 302 | + | switch (msg.parsed) { | |
| 303 | + | case ChatMessageStanza(m): | |
| 304 | + | Assert.same(["bob@example.com", "carol@example.com"], m.recipients.map(r -> r.asBare().asString())); | |
| 305 | + | default: | |
| 306 | + | Assert.fail("Expected ChatMessageStanza"); | |
| 307 | + | } | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | public function testIncomingCompatAddressingUsesRoutingJidAsChatId() { | |
| 311 | + | final stanza = new Stanza("message"); | |
| 312 | + | stanza.attr.set("type", "chat"); | |
| 313 | + | stanza.attr.set("id", "compat-in-1"); | |
| 314 | + | stanza.attr.set("from", "multi@example.com"); | |
| 315 | + | stanza.attr.set("to", "me@example.com"); | |
| 316 | + | final addresses = stanza.tag("addresses", {xmlns: "http://jabber.org/protocol/address"}); | |
| 317 | + | addresses.tag("address", {type: "replyto", jid: "multi@example.com"}).up(); | |
| 318 | + | addresses.tag("address", {type: "to", jid: "alice@example.com"}).up(); | |
| 319 | + | addresses.tag("address", {type: "to", jid: "bob@example.com"}).up(); | |
| 320 | + | addresses.tag("address", {type: "ofrom", jid: "alice@example.com"}).up(); | |
| 321 | + | addresses.up(); | |
| 322 | + | stanza.addChild(new Stanza("body").text("hi")); | |
| 323 | + | ||
| 324 | + | final msg = Message.fromStanza(stanza, JID.parse("me@example.com")); | |
| 325 | + | switch (msg.parsed) { | |
| 326 | + | case ChatMessageStanza(m): | |
| 327 | + | Assert.equals("multi@example.com", m.chatId()); | |
| 328 | + | Assert.equals("me@example.com", m.to?.asString()); | |
| 329 | + | Assert.same(["alice@example.com", "bob@example.com", "me@example.com"], m.recipients.map(r -> r.asBare().asString())); | |
| 330 | + | default: | |
| 331 | + | Assert.fail("Expected ChatMessageStanza"); | |
| 332 | + | } | |
| 333 | + | } | |
| 334 | + | ||
| 335 | + | public function testOutgoingCompatAddressingRoundTripsThroughStanza() { | |
| 336 | + | final builder = new borogove.ChatMessageBuilder(); | |
| 337 | + | builder.localId = "compat-out-1"; | |
| 338 | + | builder.direction = MessageSent; | |
| 339 | + | builder.from = JID.parse("me@example.com"); | |
| 340 | + | builder.senderId = "me@example.com"; | |
| 341 | + | builder.to = JID.parse("multi@example.com"); | |
| 342 | + | builder.recipients = [JID.parse("alice@example.com"), JID.parse("bob@example.com")]; | |
| 343 | + | builder.replyTo = [JID.parse("me@example.com")]; | |
| 344 | + | builder.setBody(borogove.Html.text("hi")); | |
| 345 | + | ||
| 346 | + | final built = builder.build(); | |
| 347 | + | Assert.equals("multi@example.com", built.chatId()); | |
| 348 | + | ||
| 349 | + | final stanza = built.asStanza(); | |
| 350 | + | Assert.equals("multi@example.com", stanza.attr.get("to")); | |
| 351 | + | final addresses = stanza.getChild("addresses", "http://jabber.org/protocol/address"); | |
| 352 | + | Assert.notNull(addresses); | |
| 353 | + | Assert.same(["alice@example.com", "bob@example.com"], addresses.allTags("address").map(a -> a.attr.get("jid"))); | |
| 354 | + | ||
| 355 | + | final reparsed = Message.fromStanza(stanza, JID.parse("me@example.com")); | |
| 356 | + | switch (reparsed.parsed) { | |
| 357 | + | case ChatMessageStanza(m): | |
| 358 | + | Assert.equals("multi@example.com", m.chatId()); | |
| 359 | + | Assert.equals("multi@example.com", m.to?.asString()); | |
| 360 | + | Assert.same(["alice@example.com", "bob@example.com"], m.recipients.map(r -> r.asBare().asString())); | |
| 361 | + | default: | |
| 362 | + | Assert.fail("Expected ChatMessageStanza"); | |
| 363 | + | } | |
| 364 | + | } | |
| 365 | + | ||
| 224 | 366 | public function testReactionFallbackSkipsNestedQuotes() { | |
| 225 | 367 | // Build a parent message that itself contains a quote and regular text | |
| 226 | 368 | final parentBuilder = new borogove.ChatMessageBuilder(); |
+27
-0
test/TestClient.hx
#
| ... | ... | @@ -595,6 +595,33 @@ class TestClient extends utest.Test { | |
| 595 | 595 | }); | |
| 596 | 596 | } | |
| 597 | 597 | ||
| 598 | + | public function testIncomingExtendedMultiDMResolvesToParticipantChatId(async: Async) { | |
| 599 | + | final persistence = new MessageMockPersistence(); | |
| 600 | + | final client = new Client("me@example.com", persistence); | |
| 601 | + | final chatId = "alice@example.com\nbob@example.com"; | |
| 602 | + | ||
| 603 | + | client.addChatsUpdatedListener(chats -> { | |
| 604 | + | final chat = chats.find(c -> c.chatId == chatId); | |
| 605 | + | if (chat == null) return; | |
| 606 | + | Assert.notNull(chat.lastMessage); | |
| 607 | + | if (chat.lastMessage != null) { | |
| 608 | + | Assert.equals(chatId, chat.lastMessage.chatId()); | |
| 609 | + | Assert.equals("hi", chat.lastMessage.body().toPlainText()); | |
| 610 | + | } | |
| 611 | + | Assert.isNull(client.getChat("alice@example.com")); | |
| 612 | + | async.done(); | |
| 613 | + | }); | |
| 614 | + | ||
| 615 | + | final stanza = new Stanza("message", { xmlns: "jabber:client", type: "chat", from: "alice@example.com", to: "me@example.com", id: "srv-multi-1" }); | |
| 616 | + | stanza.tag("body").text("hi").up(); | |
| 617 | + | stanza.tag("stanza-id", { xmlns: "urn:xmpp:sid:0", by: "me@example.com", id: "srv-multi-1" }).up(); | |
| 618 | + | final addresses = stanza.tag("addresses", { xmlns: "http://jabber.org/protocol/address" }); | |
| 619 | + | addresses.tag("address", { type: "to", jid: "alice@example.com" }).up(); | |
| 620 | + | addresses.tag("address", { type: "to", jid: "bob@example.com" }).up(); | |
| 621 | + | addresses.up(); | |
| 622 | + | client.stream.onStanza(stanza); | |
| 623 | + | } | |
| 624 | + | ||
| 598 | 625 | public function testDecryptionFailurePreservesLocalMessage(async: Async) { | |
| 599 | 626 | final persistence = new MessageMockPersistence(); | |
| 600 | 627 | final client = new Client("test@example.com", persistence); |
+142
-0
test/TestSqlite.hx
#
| ... | ... | @@ -2095,6 +2095,148 @@ class TestSqlite extends utest.Test { | |
| 2095 | 2095 | Assert.same(expected, actual); | |
| 2096 | 2096 | } | |
| 2097 | 2097 | ||
| 2098 | + | public function testCompatMultiDMOutgoingRoundTrip(async: Async) { | |
| 2099 | + | final account = "alice@example.com"; | |
| 2100 | + | final routing = "multi@example.com"; | |
| 2101 | + | final builder = new ChatMessageBuilder(); | |
| 2102 | + | builder.localId = "compat-out-1"; | |
| 2103 | + | builder.sortId = "a0"; | |
| 2104 | + | builder.direction = MessageSent; | |
| 2105 | + | builder.from = JID.parse(account); | |
| 2106 | + | builder.senderId = account; | |
| 2107 | + | builder.to = JID.parse(routing); | |
| 2108 | + | builder.recipients = [JID.parse("bob@example.com"), JID.parse("carol@example.com")]; | |
| 2109 | + | builder.replyTo = [JID.parse(account)]; | |
| 2110 | + | builder.timeReceived = "2020-01-01T00:00:00Z"; | |
| 2111 | + | builder.setBody(Html.text("hi")); | |
| 2112 | + | final message = builder.build(); | |
| 2113 | + | ||
| 2114 | + | persistence.storeMessages(account, [message]).then(_ -> { | |
| 2115 | + | Assert.equals(routing, message.chatId()); | |
| 2116 | + | return persistence.getMessagesBefore(account, routing, null); | |
| 2117 | + | }).then(result -> { | |
| 2118 | + | Assert.equals(1, result.length); | |
| 2119 | + | if (result.length < 1) { | |
| 2120 | + | async.done(); | |
| 2121 | + | return; | |
| 2122 | + | } | |
| 2123 | + | final fetched = result[0]; | |
| 2124 | + | Assert.equals(routing, fetched.chatId()); | |
| 2125 | + | Assert.equals(routing, fetched.to?.asString()); | |
| 2126 | + | Assert.same(["bob@example.com", "carol@example.com"], fetched.recipients.map(r -> r.asBare().asString())); | |
| 2127 | + | async.done(); | |
| 2128 | + | }).catchError(e -> { | |
| 2129 | + | Assert.fail(Std.string(e)); | |
| 2130 | + | async.done(); | |
| 2131 | + | }); | |
| 2132 | + | } | |
| 2133 | + | ||
| 2134 | + | public function testCompatMultiDMIncomingRoundTrip(async: Async) { | |
| 2135 | + | final account = "alice@example.com"; | |
| 2136 | + | final routing = "multi@example.com"; | |
| 2137 | + | final stanza = Stanza.parse('<message type="chat" from="multi@example.com" to="alice@example.com" id="compat-in-1"><body>hi</body><stanza-id xmlns="urn:xmpp:sid:0" by="alice@example.com" id="srv-compat-1"/><addresses xmlns="http://jabber.org/protocol/address"><address type="replyto" jid="multi@example.com"/><address type="to" jid="bob@example.com"/><address type="to" jid="carol@example.com"/><address type="ofrom" jid="bob@example.com"/></addresses></message>'); | |
| 2138 | + | final parsed = Message.fromStanza(stanza, JID.parse(account), (builder, _) -> { | |
| 2139 | + | builder.sortId = "a0"; | |
| 2140 | + | return builder; | |
| 2141 | + | }); | |
| 2142 | + | switch (parsed.parsed) { | |
| 2143 | + | case ChatMessageStanza(message): | |
| 2144 | + | persistence.storeMessages(account, [message]).then(_ -> { | |
| 2145 | + | Assert.equals(routing, message.chatId()); | |
| 2146 | + | Assert.equals(account, message.to?.asString()); | |
| 2147 | + | return persistence.getMessagesBefore(account, routing, null); | |
| 2148 | + | }).then(result -> { | |
| 2149 | + | Assert.equals(1, result.length); | |
| 2150 | + | if (result.length < 1) { | |
| 2151 | + | async.done(); | |
| 2152 | + | return; | |
| 2153 | + | } | |
| 2154 | + | final fetched = result[0]; | |
| 2155 | + | Assert.equals(routing, fetched.chatId()); | |
| 2156 | + | Assert.equals(account, fetched.to?.asString()); | |
| 2157 | + | Assert.same(["alice@example.com", "bob@example.com", "carol@example.com"], fetched.recipients.map(r -> r.asBare().asString())); | |
| 2158 | + | Assert.same([routing], fetched.replyTo.map(r -> r.asBare().asString())); | |
| 2159 | + | async.done(); | |
| 2160 | + | }).catchError(e -> { | |
| 2161 | + | Assert.fail(Std.string(e)); | |
| 2162 | + | async.done(); | |
| 2163 | + | }); | |
| 2164 | + | default: | |
| 2165 | + | Assert.fail("Expected ChatMessageStanza"); | |
| 2166 | + | async.done(); | |
| 2167 | + | } | |
| 2168 | + | } | |
| 2169 | + | ||
| 2170 | + | public function testExtendedMultiDMIncomingRoundTrip(async: Async) { | |
| 2171 | + | final account = "alice@example.com"; | |
| 2172 | + | final chatId = "bob@example.com\ncarol@example.com"; | |
| 2173 | + | final stanza = Stanza.parse('<message type="chat" from="bob@example.com" to="alice@example.com" id="extended-in-1"><body>hi</body><stanza-id xmlns="urn:xmpp:sid:0" by="alice@example.com" id="srv-extended-1"/><addresses xmlns="http://jabber.org/protocol/address"><address type="to" jid="bob@example.com"/><address type="to" jid="carol@example.com"/></addresses></message>'); | |
| 2174 | + | final parsed = Message.fromStanza(stanza, JID.parse(account), (builder, _) -> { | |
| 2175 | + | builder.sortId = "a0"; | |
| 2176 | + | return builder; | |
| 2177 | + | }); | |
| 2178 | + | switch (parsed.parsed) { | |
| 2179 | + | case ChatMessageStanza(message): | |
| 2180 | + | persistence.storeMessages(account, [message]).then(_ -> { | |
| 2181 | + | Assert.equals(chatId, message.chatId()); | |
| 2182 | + | Assert.equals(account, message.to?.asString()); | |
| 2183 | + | return persistence.getMessagesBefore(account, chatId, null); | |
| 2184 | + | }).then(result -> { | |
| 2185 | + | Assert.equals(1, result.length); | |
| 2186 | + | if (result.length < 1) { | |
| 2187 | + | async.done(); | |
| 2188 | + | return; | |
| 2189 | + | } | |
| 2190 | + | final fetched = result[0]; | |
| 2191 | + | Assert.equals(chatId, fetched.chatId()); | |
| 2192 | + | Assert.equals(account, fetched.to?.asString()); | |
| 2193 | + | Assert.same(["alice@example.com", "bob@example.com", "carol@example.com"], fetched.recipients.map(r -> r.asBare().asString())); | |
| 2194 | + | Assert.same(["bob@example.com", "carol@example.com"], fetched.replyTo.map(r -> r.asBare().asString())); | |
| 2195 | + | async.done(); | |
| 2196 | + | }).catchError(e -> { | |
| 2197 | + | Assert.fail(Std.string(e)); | |
| 2198 | + | async.done(); | |
| 2199 | + | }); | |
| 2200 | + | default: | |
| 2201 | + | Assert.fail("Expected ChatMessageStanza"); | |
| 2202 | + | async.done(); | |
| 2203 | + | } | |
| 2204 | + | } | |
| 2205 | + | ||
| 2206 | + | public function testExtendedMultiDMOutgoingRoundTrip(async: Async) { | |
| 2207 | + | final account = "alice@example.com"; | |
| 2208 | + | final chatId = "bob@example.com\ncarol@example.com"; | |
| 2209 | + | final builder = new ChatMessageBuilder(); | |
| 2210 | + | builder.localId = "extended-out-1"; | |
| 2211 | + | builder.sortId = "a0"; | |
| 2212 | + | builder.direction = MessageSent; | |
| 2213 | + | builder.from = JID.parse(account); | |
| 2214 | + | builder.senderId = account; | |
| 2215 | + | builder.to = JID.parse("bob@example.com"); | |
| 2216 | + | builder.recipients = [JID.parse("bob@example.com"), JID.parse("carol@example.com")]; | |
| 2217 | + | builder.replyTo = [JID.parse(account)]; | |
| 2218 | + | builder.timeReceived = "2020-01-01T00:00:00Z"; | |
| 2219 | + | builder.setBody(Html.text("hi")); | |
| 2220 | + | ||
| 2221 | + | persistence.storeMessages(account, [builder.build()]).then(_ -> { | |
| 2222 | + | return persistence.getMessagesBefore(account, chatId, null); | |
| 2223 | + | }).then(result -> { | |
| 2224 | + | Assert.equals(1, result.length); | |
| 2225 | + | if (result.length < 1) { | |
| 2226 | + | async.done(); | |
| 2227 | + | return; | |
| 2228 | + | } | |
| 2229 | + | final fetched = result[0]; | |
| 2230 | + | Assert.equals(chatId, fetched.chatId()); | |
| 2231 | + | Assert.equals("bob@example.com", fetched.to?.asString()); | |
| 2232 | + | Assert.same(["bob@example.com", "carol@example.com"], fetched.recipients.map(r -> r.asBare().asString())); | |
| 2233 | + | async.done(); | |
| 2234 | + | }).catchError(e -> { | |
| 2235 | + | Assert.fail(Std.string(e)); | |
| 2236 | + | async.done(); | |
| 2237 | + | }); | |
| 2238 | + | } | |
| 2239 | + | ||
| 2098 | 2240 | public function testMessageColumnsAllowsAddingColumns() { | |
| 2099 | 2241 | final defaultColumns = [ | |
| 2100 | 2242 | "stanza", |
+168
-0
test/persistence-tests.ts
#
| ... | ... | @@ -2554,6 +2554,174 @@ export function sharedPersistenceTests(test) { | |
| 2554 | 2554 | "propose", | |
| 2555 | 2555 | ]); | |
| 2556 | 2556 | }); | |
| 2557 | + | ||
| 2558 | + | test("compat multiDM outgoing keeps to and recipients separate", async ({ | |
| 2559 | + | borogove, | |
| 2560 | + | persistence, | |
| 2561 | + | }) => { | |
| 2562 | + | const account = "alice@example.com"; | |
| 2563 | + | const routing = "multi@example.com"; | |
| 2564 | + | const builder = new borogove.ChatMessageBuilder({ | |
| 2565 | + | localId: "compat-out-1", | |
| 2566 | + | senderId: account, | |
| 2567 | + | direction: borogove.MessageDirection.MessageSent, | |
| 2568 | + | }); | |
| 2569 | + | builder.sortId = "compat-out-a0"; | |
| 2570 | + | builder.from = borogove.JID.parse(account); | |
| 2571 | + | builder.to = borogove.JID.parse(routing); | |
| 2572 | + | builder.recipients = [ | |
| 2573 | + | borogove.JID.parse("bob@example.com"), | |
| 2574 | + | borogove.JID.parse("carol@example.com"), | |
| 2575 | + | ]; | |
| 2576 | + | builder.replyTo = [borogove.JID.parse(account)]; | |
| 2577 | + | builder.timeReceived = "2020-01-01T00:00:00Z"; | |
| 2578 | + | const message = builder.build(); | |
| 2579 | + | expect(message.chatId()).toBe(routing); | |
| 2580 | + | ||
| 2581 | + | await persistence.storeMessages(account, [message]); | |
| 2582 | + | const messages = await persistence.getMessagesBefore(account, routing); | |
| 2583 | + | expect(messages.length).toBe(1); | |
| 2584 | + | if (messages.length < 1) return; | |
| 2585 | + | const [fetched] = messages; | |
| 2586 | + | expect(fetched.chatId()).toBe(routing); | |
| 2587 | + | expect(fetched.to?.asString()).toBe(routing); | |
| 2588 | + | expect(fetched.recipients.map((r) => r.asBare().asString())).toEqual([ | |
| 2589 | + | "bob@example.com", | |
| 2590 | + | "carol@example.com", | |
| 2591 | + | ]); | |
| 2592 | + | }); | |
| 2593 | + | ||
| 2594 | + | test("compat multiDM incoming keeps routing jid as chat id", async ({ | |
| 2595 | + | borogove, | |
| 2596 | + | persistence, | |
| 2597 | + | }) => { | |
| 2598 | + | const account = "alice@example.com"; | |
| 2599 | + | const routing = "multi@example.com"; | |
| 2600 | + | const stanza = borogove.Stanza.parse( | |
| 2601 | + | '<message type="chat" from="multi@example.com" to="alice@example.com" id="compat-in-1"><body>hi</body><stanza-id xmlns="urn:xmpp:sid:0" by="alice@example.com" id="compat-in-srv-1"/><addresses xmlns="http://jabber.org/protocol/address"><address type="replyto" jid="multi@example.com"/><address type="to" jid="bob@example.com"/><address type="to" jid="carol@example.com"/><address type="ofrom" jid="bob@example.com"/></addresses></message>', | |
| 2602 | + | ); | |
| 2603 | + | const builder = new borogove.ChatMessageBuilder({ | |
| 2604 | + | serverId: "compat-in-srv-1", | |
| 2605 | + | serverIdBy: account, | |
| 2606 | + | senderId: "bob@example.com", | |
| 2607 | + | direction: borogove.MessageDirection.MessageReceived, | |
| 2608 | + | }); | |
| 2609 | + | builder.sortId = "compat-in-a0"; | |
| 2610 | + | builder.from = borogove.JID.parse(routing); | |
| 2611 | + | builder.to = borogove.JID.parse(account); | |
| 2612 | + | builder.recipients = [ | |
| 2613 | + | borogove.JID.parse(account), | |
| 2614 | + | borogove.JID.parse("bob@example.com"), | |
| 2615 | + | borogove.JID.parse("carol@example.com"), | |
| 2616 | + | ]; | |
| 2617 | + | builder.replyTo = [borogove.JID.parse(routing)]; | |
| 2618 | + | builder.timeReceived = "2020-01-01T00:00:01Z"; | |
| 2619 | + | (builder as any).stanza = stanza; | |
| 2620 | + | const message = builder.build(); | |
| 2621 | + | expect(message.chatId()).toBe(routing); | |
| 2622 | + | ||
| 2623 | + | await persistence.storeMessages(account, [message]); | |
| 2624 | + | const messages = await persistence.getMessagesBefore(account, routing); | |
| 2625 | + | expect(messages.length).toBe(1); | |
| 2626 | + | if (messages.length < 1) return; | |
| 2627 | + | const [fetched] = messages; | |
| 2628 | + | expect(fetched.chatId()).toBe(routing); | |
| 2629 | + | expect(fetched.to?.asString()).toBe(account); | |
| 2630 | + | expect(fetched.recipients.map((r) => r.asBare().asString())).toEqual([ | |
| 2631 | + | "alice@example.com", | |
| 2632 | + | "bob@example.com", | |
| 2633 | + | "carol@example.com", | |
| 2634 | + | ]); | |
| 2635 | + | expect(fetched.replyTo.map((r) => r.asBare().asString())).toEqual([ | |
| 2636 | + | routing, | |
| 2637 | + | ]); | |
| 2638 | + | }); | |
| 2639 | + | ||
| 2640 | + | test("extended multiDM outgoing round trips participant chat id", async ({ | |
| 2641 | + | borogove, | |
| 2642 | + | persistence, | |
| 2643 | + | }) => { | |
| 2644 | + | const account = "alice@example.com"; | |
| 2645 | + | const chatId = "bob@example.com\ncarol@example.com"; | |
| 2646 | + | const builder = new borogove.ChatMessageBuilder({ | |
| 2647 | + | localId: "extended-out-1", | |
| 2648 | + | senderId: account, | |
| 2649 | + | direction: borogove.MessageDirection.MessageSent, | |
| 2650 | + | }); | |
| 2651 | + | builder.sortId = "extended-out-a0"; | |
| 2652 | + | builder.from = borogove.JID.parse(account); | |
| 2653 | + | builder.to = borogove.JID.parse("bob@example.com"); | |
| 2654 | + | builder.recipients = [ | |
| 2655 | + | borogove.JID.parse("bob@example.com"), | |
| 2656 | + | borogove.JID.parse("carol@example.com"), | |
| 2657 | + | ]; | |
| 2658 | + | builder.replyTo = [borogove.JID.parse(account)]; | |
| 2659 | + | builder.timeReceived = "2020-01-01T00:00:00Z"; | |
| 2660 | + | const message = builder.build(); | |
| 2661 | + | expect(message.chatId()).toBe(chatId); | |
| 2662 | + | ||
| 2663 | + | await persistence.storeMessages(account, [message]); | |
| 2664 | + | const messages = await persistence.getMessagesBefore(account, chatId); | |
| 2665 | + | expect(messages.length).toBe(1); | |
| 2666 | + | if (messages.length < 1) return; | |
| 2667 | + | const [fetched] = messages; | |
| 2668 | + | expect(fetched.chatId()).toBe(chatId); | |
| 2669 | + | expect(fetched.to?.asString()).toBe("bob@example.com"); | |
| 2670 | + | expect(fetched.recipients.map((r) => r.asBare().asString())).toEqual([ | |
| 2671 | + | "bob@example.com", | |
| 2672 | + | "carol@example.com", | |
| 2673 | + | ]); | |
| 2674 | + | }); | |
| 2675 | + | ||
| 2676 | + | test("extended multiDM incoming round trips participant chat id", async ({ | |
| 2677 | + | borogove, | |
| 2678 | + | persistence, | |
| 2679 | + | }) => { | |
| 2680 | + | const account = "alice@example.com"; | |
| 2681 | + | const chatId = "bob@example.com\ncarol@example.com"; | |
| 2682 | + | const stanza = borogove.Stanza.parse( | |
| 2683 | + | '<message type="chat" from="bob@example.com" to="alice@example.com" id="extended-in-1"><body>hi</body><stanza-id xmlns="urn:xmpp:sid:0" by="alice@example.com" id="extended-in-srv-1"/><addresses xmlns="http://jabber.org/protocol/address"><address type="to" jid="bob@example.com"/><address type="to" jid="carol@example.com"/></addresses></message>', | |
| 2684 | + | ); | |
| 2685 | + | const builder = new borogove.ChatMessageBuilder({ | |
| 2686 | + | serverId: "extended-in-srv-1", | |
| 2687 | + | serverIdBy: account, | |
| 2688 | + | senderId: "bob@example.com", | |
| 2689 | + | direction: borogove.MessageDirection.MessageReceived, | |
| 2690 | + | }); | |
| 2691 | + | builder.sortId = "extended-in-a0"; | |
| 2692 | + | builder.from = borogove.JID.parse("bob@example.com"); | |
| 2693 | + | builder.to = borogove.JID.parse(account); | |
| 2694 | + | builder.recipients = [ | |
| 2695 | + | borogove.JID.parse(account), | |
| 2696 | + | borogove.JID.parse("bob@example.com"), | |
| 2697 | + | borogove.JID.parse("carol@example.com"), | |
| 2698 | + | ]; | |
| 2699 | + | builder.replyTo = [ | |
| 2700 | + | borogove.JID.parse("bob@example.com"), | |
| 2701 | + | borogove.JID.parse("carol@example.com"), | |
| 2702 | + | ]; | |
| 2703 | + | builder.timeReceived = "2020-01-01T00:00:02Z"; | |
| 2704 | + | (builder as any).stanza = stanza; | |
| 2705 | + | const message = builder.build(); | |
| 2706 | + | expect(message.chatId()).toBe(chatId); | |
| 2707 | + | ||
| 2708 | + | await persistence.storeMessages(account, [message]); | |
| 2709 | + | const messages = await persistence.getMessagesBefore(account, chatId); | |
| 2710 | + | expect(messages.length).toBe(1); | |
| 2711 | + | if (messages.length < 1) return; | |
| 2712 | + | const [fetched] = messages; | |
| 2713 | + | expect(fetched.chatId()).toBe(chatId); | |
| 2714 | + | expect(fetched.to?.asString()).toBe(account); | |
| 2715 | + | expect(fetched.recipients.map((r) => r.asBare().asString())).toEqual([ | |
| 2716 | + | "alice@example.com", | |
| 2717 | + | "bob@example.com", | |
| 2718 | + | "carol@example.com", | |
| 2719 | + | ]); | |
| 2720 | + | expect(fetched.replyTo.map((r) => r.asBare().asString())).toEqual([ | |
| 2721 | + | "bob@example.com", | |
| 2722 | + | "carol@example.com", | |
| 2723 | + | ]); | |
| 2724 | + | }); | |
| 2557 | 2725 | } | |
| 2558 | 2726 | ||
| 2559 | 2727 | type TestKeyPair = { |