| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-04-21 19:10:00 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-04-21 19:10:00 UTC |
| parent | 2875dd7358103628b7187dce5fa98ad600df12d9 |
| borogove/Chat.hx | +8 | -3 |
| borogove/Client.hx | +11 | -0 |
| test/TestChat.hx | +75 | -0 |
diff --git a/borogove/Chat.hx b/borogove/Chat.hx index 9eee475..8c4db7a 100644 --- a/borogove/Chat.hx +++ b/borogove/Chat.hx @@ -1299,6 +1299,8 @@ class Channel extends Chat { private var disco: Caps = new Caps("", [], ["http://jabber.org/protocol/muc"], []); @:allow(borogove) private var inSync = false; + @:allow(borogove) + private var joinFailed = null; private var sync = null; private var forceLive = false; private var _nickInUse = null; @@ -1601,6 +1603,8 @@ class Channel extends Chat { doSync(null, syncPoint.sortId); } else { trace("SYNC failed", chatId, stanza); + joinFailed = stanza; + client.trigger("chats/update", [this]); } }); sync.fetchNext(); @@ -1672,7 +1676,7 @@ trace("XYZZY no MUC avatar locally matching so fetch vcard", chatId, avatarSha1H } override public function syncing() { - return !inSync || !livePresence(); + return sync != null || (!livePresence() && joinFailed == null); } override private function setLastMessage(message:Null<ChatMessage>) { @@ -1692,6 +1696,7 @@ trace("XYZZY no MUC avatar locally matching so fetch vcard", chatId, avatarSha1H return _nickInUse ?? client.displayName(); } + @:allow(borogove) private function getFullJid() { return JID.parse(chatId).withResource(nickInUse()); } @@ -1754,7 +1759,7 @@ trace("XYZZY no MUC avatar locally matching so fetch vcard", chatId, avatarSha1H @HaxeCBridge.noemit // on superclass as abstract public function getMessagesAfter(after: Null<ChatMessage>):Promise<Array<ChatMessage>> { if (after != null && after.chatId() != chatId) throw "Cannot look after from a different chat"; - if (after != null && lastMessage != null && lastMessage.canReplace(after) && !syncing()) { + if (after != null && lastMessage != null && lastMessage.canReplace(after) && inSync) { return Promise.resolve([]); } @@ -1785,7 +1790,7 @@ trace("XYZZY no MUC avatar locally matching so fetch vcard", chatId, avatarSha1H @:allow(borogove) private function prepareIncomingMessage(message:ChatMessageBuilder, stanza:Stanza) { - message.syncPoint = !syncing(); + message.syncPoint = inSync; if (message.type == MessageChat) message.type = MessageChannelPrivate; if (message.sortId == null) { if (sortId != null && message.type == MessageChannel) { diff --git a/borogove/Client.hx b/borogove/Client.hx index bcc8244..383e809 100644 --- a/borogove/Client.hx +++ b/borogove/Client.hx @@ -442,6 +442,17 @@ class Client extends EventEmitter { } } + if (stanza.attr.get("from") != null && stanza.attr.get("type") == "error") { + final from = JID.parse(stanza.attr.get("from")); + final chat = getChat(from.asBare().asString()); + final channel = Std.downcast(chat, Channel); + if (channel != null && from.asString() == channel.getFullJid().asString()) { + trace("Join failed", channel.chatId, stanza); + channel.joinFailed = stanza; + this.trigger("chats/update", [channel]); + } + } + return EventUnhandled; }); } diff --git a/test/TestChat.hx b/test/TestChat.hx index 4c6323c..6ae661d 100644 --- a/test/TestChat.hx +++ b/test/TestChat.hx @@ -7,6 +7,9 @@ import borogove.ChatMessageBuilder; import borogove.Stanza; import borogove.JID; import borogove.persistence.Dummy; +import borogove.Chat.Channel; +import borogove.Chat.AvailableChat; +import borogove.Caps.Identity; @:access(borogove) class TestChat extends utest.Test { @@ -276,4 +279,76 @@ class TestChat extends utest.Test { chat.presence.set("mynick", p2); Assert.isTrue(chat.canModerate()); } + + public function testJoinFailure() { + final persistence = new Dummy(); + final client = new Client("test@example.com", persistence); + final caps = new borogove.Caps("", [new Identity("conference", "text", "Channel")], ["http://jabber.org/protocol/muc"], []); + final availableChat = new AvailableChat("channel@example.com", "Channel", "", caps); + final channel = cast(client.startChat(availableChat), Channel); + + Assert.isTrue(channel.syncing(), "Should be syncing initially"); + + final errorStanza = new Stanza("presence", { xmlns: "jabber:client", from: "channel@example.com/test", to: "test@example.com", type: "error" }) + .tag("error", { type: "auth" }) + .tag("forbidden", { xmlns: "urn:ietf:params:xml:ns:xmpp-stanzas" }).up() + .up(); + + client.stream.onStanza(errorStanza); + + Assert.equals(channel.joinFailed, errorStanza, "joinFailed should be set"); + Assert.isFalse(channel.syncing(), "Should NOT be syncing after join failure"); + } + + public function testSyncFailure(async: Async) { + final persistence = new Dummy(); + final client = new Client("test@example.com", persistence); + final caps = new borogove.Caps("", [new Identity("conference", "text", "Channel")], ["http://jabber.org/protocol/muc", "urn:xmpp:mam:2"], []); + final availableChat = new AvailableChat("channel@example.com", "Channel", "", caps); + final channel = cast(client.startChat(availableChat), Channel); + + Assert.isTrue(channel.syncing(), "Should be syncing initially"); + + client.stream.on("sendStanza", (stanza: Stanza) -> { + if (stanza.name == "iq") { + // Delay of 0 to force async like in real life + haxe.Timer.delay(() -> { + final errorStanza = new Stanza("iq", { xmlns: "jabber:client", type: "error", id: stanza.attr.get("id") }) + .tag("error", { type: "cancel" }) + .tag("feature-not-implemented", { xmlns: "urn:ietf:params:xml:ns:xmpp-stanzas" }).up() + .up(); + + client.stream.onStanza(errorStanza); + + Assert.isNull(channel.sync, "sync should be cleared after failure"); + Assert.isFalse(channel.inSync, "Should not be inSync"); + Assert.isFalse(channel.syncing(), "Should NOT be syncing after sync failure"); + async.done(); + }, 0); + } + + return EventHandled; + }); + + channel.doSync(null); + Assert.notNull(channel.sync, "sync should be set during sync"); + Assert.isTrue(channel.syncing(), "Should be syncing during sync"); + } + + public function testSyncPointWhenNotInSync() { + final persistence = new Dummy(); + final client = new Client("test@example.com", persistence); + final channel = new Channel(client, client.stream, persistence, "channel@example.com"); + + channel.inSync = false; + final builder = new ChatMessageBuilder(); + final stanza = new Stanza("message", { from: "channel@example.com/someone" }); + channel.prepareIncomingMessage(builder, stanza); + + Assert.isFalse(builder.syncPoint, "Message should NOT have syncPoint if NOT inSync"); + + channel.inSync = true; + channel.prepareIncomingMessage(builder, stanza); + Assert.isTrue(builder.syncPoint, "Message SHOULD have syncPoint if inSync"); + } }