borogove

created pr with borogove/multi-dm.1 on 2026-11-09T19:47:55Z · by 9e834e0d
added borogove/multi-dm.2 on 2026-11-10T19:08:22Z · by 9e834e0d
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 -3
checkout specific patchset revision:
ssh pr.pico.sh pull borogove:multi-dm [rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh borogove:multi-dm
add comment to patch request:
ssh pr.pico.sh comment borogove:multi-dm "lgtm!"
+5 -0 borogove/Message.hx #
......@@ -179,6 +179,11 @@ class Message {
179179 final addresses = stanza.getChild("addresses", "http://jabber.org/protocol/address");
180180 var anyExtendedReplyTo = false;
181181 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+ }
182187 for (address in addresses.allTags("address")) {
183188 final jid = address.attr.get("jid");
184189 if (address.attr.get("type") == "noreply") {
+142 -0 test/TestChatMessage.hx #
......@@ -221,6 +221,148 @@ class TestChatMessage extends utest.Test {
221221 Assert.equals("> line one\n> line two\n> line three\n\nok sure", body);
222222 }
223223
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+
224366 public function testReactionFallbackSkipsNestedQuotes() {
225367 // Build a parent message that itself contains a quote and regular text
226368 final parentBuilder = new borogove.ChatMessageBuilder();
+27 -0 test/TestClient.hx #
......@@ -595,6 +595,33 @@ class TestClient extends utest.Test {
595595 });
596596 }
597597
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+
598625 public function testDecryptionFailurePreservesLocalMessage(async: Async) {
599626 final persistence = new MessageMockPersistence();
600627 final client = new Client("test@example.com", persistence);
+142 -0 test/TestSqlite.hx #
......@@ -2095,6 +2095,148 @@ class TestSqlite extends utest.Test {
20952095 Assert.same(expected, actual);
20962096 }
20972097
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+
20982240 public function testMessageColumnsAllowsAddingColumns() {
20992241 final defaultColumns = [
21002242 "stanza",
+168 -0 test/persistence-tests.ts #
......@@ -2554,6 +2554,174 @@ export function sharedPersistenceTests(test) {
25542554 "propose",
25552555 ]);
25562556 });
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+ });
25572725 }
25582726
25592727 type TestKeyPair = {
Back to top