- Bookmarks (XEP-0402) fetched on every sync; autojoin rooms are joined (no MUC history requested) and caught up from the room's MAM archive (newest 200 on first join, then from the last archive id). Joining a room from "+" bookmarks it with autojoin. - Join handling: own presence (110) confirms, server nick changes (210) are adopted, nick conflicts retry with a suffix, join errors and kicks/bans show in the header; rooms re-join after reconnect. - Own room messages recognised by nick and marked delivered on reflection; mentions of our nick highlighted and flagged in the list; "nick: message" previews; room names from bookmarks. - Private messages through a room get their own conversation (full JID), fixing the gap noted in milestone 2. - Rooms get no typing states, no contact actions; the composer waits for the join. - 3 more parser tests (11). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
6.2 KiB
TypeScript
105 lines
6.2 KiB
TypeScript
// Run: npm test (Node strips the TypeScript types itself)
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import parse from "ltx/lib/parse.js";
|
|
import { parseAck, parseMessage, parseOwnDisplayed } from "../src/xmpp/parse.ts";
|
|
|
|
const me = "lukas@0ff.space";
|
|
const el = (s: string) => parse(s) as any;
|
|
|
|
test("live incoming message", () => {
|
|
const m = parseMessage(el(`<message from="anna@0ff.space/phone" to="lukas@0ff.space/web" type="chat" id="m1">
|
|
<body>hi</body><origin-id xmlns="urn:xmpp:sid:0" id="o1"/>
|
|
<stanza-id xmlns="urn:xmpp:sid:0" by="lukas@0ff.space" id="A1"/>
|
|
<request xmlns="urn:xmpp:receipts"/><markable xmlns="urn:xmpp:chat-markers:0"/></message>`), { me })!;
|
|
assert.equal(m.peer, "anna@0ff.space");
|
|
assert.equal(m.dir, "in");
|
|
assert.equal(m.key, "anna@0ff.space|anna@0ff.space|o1");
|
|
assert.equal(m.archiveId, "A1");
|
|
assert.equal(m.receiptRequested, true);
|
|
assert.equal(m.markable, true);
|
|
});
|
|
|
|
test("stanza-id stamped by someone else is ignored", () => {
|
|
const m = parseMessage(el(`<message from="anna@0ff.space/phone" type="chat" id="m1"><body>x</body>
|
|
<stanza-id xmlns="urn:xmpp:sid:0" by="anna@0ff.space" id="FORGED"/></message>`), { me })!;
|
|
assert.equal(m.archiveId, undefined);
|
|
});
|
|
|
|
test("sent carbon and MAM copy of the same message share one key", () => {
|
|
const inner = `<message from="lukas@0ff.space/monal" to="anna@0ff.space" type="chat" id="m2"><body>yo</body></message>`;
|
|
const fwd = el(`<forwarded xmlns="urn:xmpp:forward:0"><delay xmlns="urn:xmpp:delay" stamp="2026-09-28T10:00:00Z"/>${inner}</forwarded>`);
|
|
const carbon = parseMessage(el(inner), { me })!;
|
|
const mam = parseMessage(fwd.getChild("message"), { me, archiveId: "A2", forwarded: fwd })!;
|
|
assert.equal(carbon.dir, "out");
|
|
assert.equal(carbon.peer, "anna@0ff.space");
|
|
assert.equal(carbon.key, mam.key);
|
|
assert.equal(mam.archiveId, "A2");
|
|
assert.equal(mam.ts, Date.parse("2026-09-28T10:00:00Z"));
|
|
assert.equal(mam.status, "sent");
|
|
});
|
|
|
|
test("groupchat message keys by room/nick and trusts the room's stanza-id", () => {
|
|
const m = parseMessage(el(`<message from="lobby@muc.0ff.space/anna" type="groupchat" id="g1"><body>hey</body>
|
|
<stanza-id xmlns="urn:xmpp:sid:0" by="lobby@muc.0ff.space" id="R1"/></message>`), { me })!;
|
|
assert.equal(m.peer, "lobby@muc.0ff.space");
|
|
assert.equal(m.key, "lobby@muc.0ff.space|lobby@muc.0ff.space/anna|g1");
|
|
assert.equal(m.archiveId, "R1");
|
|
});
|
|
|
|
test("non-messages are skipped", () => {
|
|
assert.equal(parseMessage(el(`<message from="anna@0ff.space" type="chat"><composing xmlns="http://jabber.org/protocol/chatstates"/></message>`), { me }), null);
|
|
assert.equal(parseMessage(el(`<message from="anna@0ff.space" type="error" id="x"><body>b</body></message>`), { me }), null);
|
|
});
|
|
|
|
test("receipts and markers point at our outgoing message", () => {
|
|
assert.deepEqual(parseAck(el(`<message from="anna@0ff.space/phone"><received xmlns="urn:xmpp:receipts" id="m2"/></message>`), me),
|
|
{ key: "anna@0ff.space|lukas@0ff.space|m2", status: "delivered" });
|
|
assert.deepEqual(parseAck(el(`<message from="anna@0ff.space/phone" type="chat"><displayed xmlns="urn:xmpp:chat-markers:0" id="m2"/></message>`), me),
|
|
{ key: "anna@0ff.space|lukas@0ff.space|m2", status: "read" });
|
|
// Our own receipt echoed as a carbon is not about our messages.
|
|
assert.equal(parseAck(el(`<message from="lukas@0ff.space/monal" to="anna@0ff.space"><received xmlns="urn:xmpp:receipts" id="m1"/></message>`), me), null);
|
|
});
|
|
|
|
test("our own displayed marker from another device marks the peer's message read", () => {
|
|
// Inner message of a sent carbon: Monal told anna it displayed her message m1.
|
|
assert.deepEqual(parseOwnDisplayed(el(`<message from="lukas@0ff.space/monal" to="anna@0ff.space" type="chat"><displayed xmlns="urn:xmpp:chat-markers:0" id="m1"/></message>`), me),
|
|
{ peer: "anna@0ff.space", key: "anna@0ff.space|anna@0ff.space|m1" });
|
|
// Anna's marker is not ours.
|
|
assert.equal(parseOwnDisplayed(el(`<message from="anna@0ff.space/p" to="lukas@0ff.space" type="chat"><displayed xmlns="urn:xmpp:chat-markers:0" id="m2"/></message>`), me), null);
|
|
});
|
|
|
|
test("id-less messages get a stable key, so redelivery does not duplicate", () => {
|
|
const motd = `<message from="0ff.space" to="lukas@0ff.space/web"><body>Welcome!</body></message>`;
|
|
const a = parseMessage(el(motd), { me })!;
|
|
const b = parseMessage(el(motd), { me })!;
|
|
assert.equal(a.key, b.key);
|
|
assert.equal(a.peer, "0ff.space");
|
|
});
|
|
|
|
test("our own room message is recognised by nick and counts as delivered", () => {
|
|
const m = parseMessage(el(`<message from="lobby@muc.0ff.space/lukas" type="groupchat" id="g2"><body>hi all</body></message>`), { me, nick: "lukas" })!;
|
|
assert.equal(m.dir, "out");
|
|
assert.equal(m.status, "delivered");
|
|
// Same key the client stores when sending as lukas in that room.
|
|
assert.equal(m.key, "lobby@muc.0ff.space|lobby@muc.0ff.space/lukas|g2");
|
|
});
|
|
|
|
test("mentions of our nick in rooms, whole word, any case", () => {
|
|
const msg = (body: string) => parseMessage(el(`<message from="lobby@muc.0ff.space/anna" type="groupchat" id="x"><body>${body}</body></message>`), { me, nick: "lukas" })!;
|
|
assert.equal(msg("@Lukas can you check?").mention, true);
|
|
assert.equal(msg("lukas: ping").mention, true);
|
|
assert.equal(msg("lukasz is here").mention, undefined);
|
|
assert.equal(parseMessage(el(`<message from="lobby@muc.0ff.space/lukas" type="groupchat" id="y"><body>lukas here</body></message>`), { me, nick: "lukas" })!.mention, undefined);
|
|
});
|
|
|
|
test("private messages through a room are their own conversation", () => {
|
|
const isRoom = (j: string) => j.endsWith("@muc.0ff.space");
|
|
const incoming = parseMessage(el(`<message from="lobby@muc.0ff.space/anna" to="lukas@0ff.space/web" type="chat" id="p1"><body>psst</body></message>`), { me, isRoom })!;
|
|
assert.equal(incoming.peer, "lobby@muc.0ff.space/anna");
|
|
assert.equal(incoming.dir, "in");
|
|
// Our reply as a carbon from another device lands in the same conversation.
|
|
const out = parseMessage(el(`<message from="lukas@0ff.space/monal" to="lobby@muc.0ff.space/anna" type="chat" id="p2"><body>yes?</body></message>`), { me, isRoom })!;
|
|
assert.equal(out.peer, "lobby@muc.0ff.space/anna");
|
|
assert.equal(out.key, "lobby@muc.0ff.space/anna|lukas@0ff.space|p2");
|
|
});
|