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 #
......@@ -620,6 +620,33 @@ class TestClient extends utest.Test {
620620 });
621621 }
622622
623+ public function testIncomingExtendedMultiDMResolvesToParticipantChatId(async: Async) {
624+ final persistence = new MessageMockPersistence();
625+ final client = new Client("me@example.com", persistence);
626+ final chatId = "alice@example.com\nbob@example.com";
627+
628+ client.addChatsUpdatedListener(chats -> {
629+ final chat = chats.find(c -> c.chatId == chatId);
630+ if (chat == null) return;
631+ Assert.notNull(chat.lastMessage);
632+ if (chat.lastMessage != null) {
633+ Assert.equals(chatId, chat.lastMessage.chatId());
634+ Assert.equals("hi", chat.lastMessage.body().toPlainText());
635+ }
636+ Assert.isNull(client.getChat("alice@example.com"));
637+ async.done();
638+ });
639+
640+ final stanza = new Stanza("message", { xmlns: "jabber:client", type: "chat", from: "alice@example.com", to: "me@example.com", id: "srv-multi-1" });
641+ stanza.tag("body").text("hi").up();
642+ stanza.tag("stanza-id", { xmlns: "urn:xmpp:sid:0", by: "me@example.com", id: "srv-multi-1" }).up();
643+ final addresses = stanza.tag("addresses", { xmlns: "http://jabber.org/protocol/address" });
644+ addresses.tag("address", { type: "to", jid: "alice@example.com" }).up();
645+ addresses.tag("address", { type: "to", jid: "bob@example.com" }).up();
646+ addresses.up();
647+ client.stream.onStanza(stanza);
648+ }
649+
623650 public function testDecryptionFailurePreservesLocalMessage(async: Async) {
624651 final persistence = new MessageMockPersistence();
625652 final client = new Client("test@example.com", persistence);
+142 -0 test/TestSqlite.hx #
......@@ -2124,6 +2124,148 @@ class TestSqlite extends utest.Test {
21242124 Assert.same(expected, actual);
21252125 }
21262126
2127+ public function testCompatMultiDMOutgoingRoundTrip(async: Async) {
2128+ final account = "alice@example.com";
2129+ final routing = "multi@example.com";
2130+ final builder = new ChatMessageBuilder();
2131+ builder.localId = "compat-out-1";
2132+ builder.sortId = "a0";
2133+ builder.direction = MessageSent;
2134+ builder.from = JID.parse(account);
2135+ builder.senderId = account;
2136+ builder.to = JID.parse(routing);
2137+ builder.recipients = [JID.parse("bob@example.com"), JID.parse("carol@example.com")];
2138+ builder.replyTo = [JID.parse(account)];
2139+ builder.timeReceived = "2020-01-01T00:00:00Z";
2140+ builder.setBody(Html.text("hi"));
2141+ final message = builder.build();
2142+
2143+ persistence.storeMessages(account, [message]).then(_ -> {
2144+ Assert.equals(routing, message.chatId());
2145+ return persistence.getMessagesBefore(account, routing, null);
2146+ }).then(result -> {
2147+ Assert.equals(1, result.length);
2148+ if (result.length < 1) {
2149+ async.done();
2150+ return;
2151+ }
2152+ final fetched = result[0];
2153+ Assert.equals(routing, fetched.chatId());
2154+ Assert.equals(routing, fetched.to?.asString());
2155+ Assert.same(["bob@example.com", "carol@example.com"], fetched.recipients.map(r -> r.asBare().asString()));
2156+ async.done();
2157+ }).catchError(e -> {
2158+ Assert.fail(Std.string(e));
2159+ async.done();
2160+ });
2161+ }
2162+
2163+ public function testCompatMultiDMIncomingRoundTrip(async: Async) {
2164+ final account = "alice@example.com";
2165+ final routing = "multi@example.com";
2166+ 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>');
2167+ final parsed = Message.fromStanza(stanza, JID.parse(account), (builder, _) -> {
2168+ builder.sortId = "a0";
2169+ return builder;
2170+ });
2171+ switch (parsed.parsed) {
2172+ case ChatMessageStanza(message):
2173+ persistence.storeMessages(account, [message]).then(_ -> {
2174+ Assert.equals(routing, message.chatId());
2175+ Assert.equals(account, message.to?.asString());
2176+ return persistence.getMessagesBefore(account, routing, null);
2177+ }).then(result -> {
2178+ Assert.equals(1, result.length);
2179+ if (result.length < 1) {
2180+ async.done();
2181+ return;
2182+ }
2183+ final fetched = result[0];
2184+ Assert.equals(routing, fetched.chatId());
2185+ Assert.equals(account, fetched.to?.asString());
2186+ Assert.same(["alice@example.com", "bob@example.com", "carol@example.com"], fetched.recipients.map(r -> r.asBare().asString()));
2187+ Assert.same([routing], fetched.replyTo.map(r -> r.asBare().asString()));
2188+ async.done();
2189+ }).catchError(e -> {
2190+ Assert.fail(Std.string(e));
2191+ async.done();
2192+ });
2193+ default:
2194+ Assert.fail("Expected ChatMessageStanza");
2195+ async.done();
2196+ }
2197+ }
2198+
2199+ public function testExtendedMultiDMIncomingRoundTrip(async: Async) {
2200+ final account = "alice@example.com";
2201+ final chatId = "bob@example.com\ncarol@example.com";
2202+ 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>');
2203+ final parsed = Message.fromStanza(stanza, JID.parse(account), (builder, _) -> {
2204+ builder.sortId = "a0";
2205+ return builder;
2206+ });
2207+ switch (parsed.parsed) {
2208+ case ChatMessageStanza(message):
2209+ persistence.storeMessages(account, [message]).then(_ -> {
2210+ Assert.equals(chatId, message.chatId());
2211+ Assert.equals(account, message.to?.asString());
2212+ return persistence.getMessagesBefore(account, chatId, null);
2213+ }).then(result -> {
2214+ Assert.equals(1, result.length);
2215+ if (result.length < 1) {
2216+ async.done();
2217+ return;
2218+ }
2219+ final fetched = result[0];
2220+ Assert.equals(chatId, fetched.chatId());
2221+ Assert.equals(account, fetched.to?.asString());
2222+ Assert.same(["alice@example.com", "bob@example.com", "carol@example.com"], fetched.recipients.map(r -> r.asBare().asString()));
2223+ Assert.same(["bob@example.com", "carol@example.com"], fetched.replyTo.map(r -> r.asBare().asString()));
2224+ async.done();
2225+ }).catchError(e -> {
2226+ Assert.fail(Std.string(e));
2227+ async.done();
2228+ });
2229+ default:
2230+ Assert.fail("Expected ChatMessageStanza");
2231+ async.done();
2232+ }
2233+ }
2234+
2235+ public function testExtendedMultiDMOutgoingRoundTrip(async: Async) {
2236+ final account = "alice@example.com";
2237+ final chatId = "bob@example.com\ncarol@example.com";
2238+ final builder = new ChatMessageBuilder();
2239+ builder.localId = "extended-out-1";
2240+ builder.sortId = "a0";
2241+ builder.direction = MessageSent;
2242+ builder.from = JID.parse(account);
2243+ builder.senderId = account;
2244+ builder.to = JID.parse("bob@example.com");
2245+ builder.recipients = [JID.parse("bob@example.com"), JID.parse("carol@example.com")];
2246+ builder.replyTo = [JID.parse(account)];
2247+ builder.timeReceived = "2020-01-01T00:00:00Z";
2248+ builder.setBody(Html.text("hi"));
2249+
2250+ persistence.storeMessages(account, [builder.build()]).then(_ -> {
2251+ return persistence.getMessagesBefore(account, chatId, null);
2252+ }).then(result -> {
2253+ Assert.equals(1, result.length);
2254+ if (result.length < 1) {
2255+ async.done();
2256+ return;
2257+ }
2258+ final fetched = result[0];
2259+ Assert.equals(chatId, fetched.chatId());
2260+ Assert.equals("bob@example.com", fetched.to?.asString());
2261+ Assert.same(["bob@example.com", "carol@example.com"], fetched.recipients.map(r -> r.asBare().asString()));
2262+ async.done();
2263+ }).catchError(e -> {
2264+ Assert.fail(Std.string(e));
2265+ async.done();
2266+ });
2267+ }
2268+
21272269 public function testMessageColumnsAllowsAddingColumns() {
21282270 final defaultColumns = [
21292271 "stanza",
+168 -0 test/persistence-tests.ts #
......@@ -2559,6 +2559,174 @@ export function sharedPersistenceTests(test) {
25592559 "propose",
25602560 ]);
25612561 });
2562+
2563+ test("compat multiDM outgoing keeps to and recipients separate", async ({
2564+ borogove,
2565+ persistence,
2566+ }) => {
2567+ const account = "alice@example.com";
2568+ const routing = "multi@example.com";
2569+ const builder = new borogove.ChatMessageBuilder({
2570+ localId: "compat-out-1",
2571+ senderId: account,
2572+ direction: borogove.MessageDirection.MessageSent,
2573+ });
2574+ builder.sortId = "compat-out-a0";
2575+ builder.from = borogove.JID.parse(account);
2576+ builder.to = borogove.JID.parse(routing);
2577+ builder.recipients = [
2578+ borogove.JID.parse("bob@example.com"),
2579+ borogove.JID.parse("carol@example.com"),
2580+ ];
2581+ builder.replyTo = [borogove.JID.parse(account)];
2582+ builder.timeReceived = "2020-01-01T00:00:00Z";
2583+ const message = builder.build();
2584+ expect(message.chatId()).toBe(routing);
2585+
2586+ await persistence.storeMessages(account, [message]);
2587+ const messages = await persistence.getMessagesBefore(account, routing);
2588+ expect(messages.length).toBe(1);
2589+ if (messages.length < 1) return;
2590+ const [fetched] = messages;
2591+ expect(fetched.chatId()).toBe(routing);
2592+ expect(fetched.to?.asString()).toBe(routing);
2593+ expect(fetched.recipients.map((r) => r.asBare().asString())).toEqual([
2594+ "bob@example.com",
2595+ "carol@example.com",
2596+ ]);
2597+ });
2598+
2599+ test("compat multiDM incoming keeps routing jid as chat id", async ({
2600+ borogove,
2601+ persistence,
2602+ }) => {
2603+ const account = "alice@example.com";
2604+ const routing = "multi@example.com";
2605+ const stanza = borogove.Stanza.parse(
2606+ '<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>',
2607+ );
2608+ const builder = new borogove.ChatMessageBuilder({
2609+ serverId: "compat-in-srv-1",
2610+ serverIdBy: account,
2611+ senderId: "bob@example.com",
2612+ direction: borogove.MessageDirection.MessageReceived,
2613+ });
2614+ builder.sortId = "compat-in-a0";
2615+ builder.from = borogove.JID.parse(routing);
2616+ builder.to = borogove.JID.parse(account);
2617+ builder.recipients = [
2618+ borogove.JID.parse(account),
2619+ borogove.JID.parse("bob@example.com"),
2620+ borogove.JID.parse("carol@example.com"),
2621+ ];
2622+ builder.replyTo = [borogove.JID.parse(routing)];
2623+ builder.timeReceived = "2020-01-01T00:00:01Z";
2624+ (builder as any).stanza = stanza;
2625+ const message = builder.build();
2626+ expect(message.chatId()).toBe(routing);
2627+
2628+ await persistence.storeMessages(account, [message]);
2629+ const messages = await persistence.getMessagesBefore(account, routing);
2630+ expect(messages.length).toBe(1);
2631+ if (messages.length < 1) return;
2632+ const [fetched] = messages;
2633+ expect(fetched.chatId()).toBe(routing);
2634+ expect(fetched.to?.asString()).toBe(account);
2635+ expect(fetched.recipients.map((r) => r.asBare().asString())).toEqual([
2636+ "alice@example.com",
2637+ "bob@example.com",
2638+ "carol@example.com",
2639+ ]);
2640+ expect(fetched.replyTo.map((r) => r.asBare().asString())).toEqual([
2641+ routing,
2642+ ]);
2643+ });
2644+
2645+ test("extended multiDM outgoing round trips participant chat id", async ({
2646+ borogove,
2647+ persistence,
2648+ }) => {
2649+ const account = "alice@example.com";
2650+ const chatId = "bob@example.com\ncarol@example.com";
2651+ const builder = new borogove.ChatMessageBuilder({
2652+ localId: "extended-out-1",
2653+ senderId: account,
2654+ direction: borogove.MessageDirection.MessageSent,
2655+ });
2656+ builder.sortId = "extended-out-a0";
2657+ builder.from = borogove.JID.parse(account);
2658+ builder.to = borogove.JID.parse("bob@example.com");
2659+ builder.recipients = [
2660+ borogove.JID.parse("bob@example.com"),
2661+ borogove.JID.parse("carol@example.com"),
2662+ ];
2663+ builder.replyTo = [borogove.JID.parse(account)];
2664+ builder.timeReceived = "2020-01-01T00:00:00Z";
2665+ const message = builder.build();
2666+ expect(message.chatId()).toBe(chatId);
2667+
2668+ await persistence.storeMessages(account, [message]);
2669+ const messages = await persistence.getMessagesBefore(account, chatId);
2670+ expect(messages.length).toBe(1);
2671+ if (messages.length < 1) return;
2672+ const [fetched] = messages;
2673+ expect(fetched.chatId()).toBe(chatId);
2674+ expect(fetched.to?.asString()).toBe("bob@example.com");
2675+ expect(fetched.recipients.map((r) => r.asBare().asString())).toEqual([
2676+ "bob@example.com",
2677+ "carol@example.com",
2678+ ]);
2679+ });
2680+
2681+ test("extended multiDM incoming round trips participant chat id", async ({
2682+ borogove,
2683+ persistence,
2684+ }) => {
2685+ const account = "alice@example.com";
2686+ const chatId = "bob@example.com\ncarol@example.com";
2687+ const stanza = borogove.Stanza.parse(
2688+ '<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>',
2689+ );
2690+ const builder = new borogove.ChatMessageBuilder({
2691+ serverId: "extended-in-srv-1",
2692+ serverIdBy: account,
2693+ senderId: "bob@example.com",
2694+ direction: borogove.MessageDirection.MessageReceived,
2695+ });
2696+ builder.sortId = "extended-in-a0";
2697+ builder.from = borogove.JID.parse("bob@example.com");
2698+ builder.to = borogove.JID.parse(account);
2699+ builder.recipients = [
2700+ borogove.JID.parse(account),
2701+ borogove.JID.parse("bob@example.com"),
2702+ borogove.JID.parse("carol@example.com"),
2703+ ];
2704+ builder.replyTo = [
2705+ borogove.JID.parse("bob@example.com"),
2706+ borogove.JID.parse("carol@example.com"),
2707+ ];
2708+ builder.timeReceived = "2020-01-01T00:00:02Z";
2709+ (builder as any).stanza = stanza;
2710+ const message = builder.build();
2711+ expect(message.chatId()).toBe(chatId);
2712+
2713+ await persistence.storeMessages(account, [message]);
2714+ const messages = await persistence.getMessagesBefore(account, chatId);
2715+ expect(messages.length).toBe(1);
2716+ if (messages.length < 1) return;
2717+ const [fetched] = messages;
2718+ expect(fetched.chatId()).toBe(chatId);
2719+ expect(fetched.to?.asString()).toBe(account);
2720+ expect(fetched.recipients.map((r) => r.asBare().asString())).toEqual([
2721+ "alice@example.com",
2722+ "bob@example.com",
2723+ "carol@example.com",
2724+ ]);
2725+ expect(fetched.replyTo.map((r) => r.asBare().asString())).toEqual([
2726+ "bob@example.com",
2727+ "carol@example.com",
2728+ ]);
2729+ });
25622730 }
25632731
25642732 type TestKeyPair = {
Back to top