| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-03-04 15:00:54 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-03-04 19:28:26 UTC |
| parent | 26516469a79686db309c3c0005253fb98b8ab053 |
| HaxeSwiftBridge.hx | +11 | -0 |
| borogove/AvailableChatIterator.hx | +166 | -0 |
| borogove/Client.hx | +6 | -113 |
diff --git a/HaxeSwiftBridge.hx b/HaxeSwiftBridge.hx index 46d5763..62315aa 100644 --- a/HaxeSwiftBridge.hx +++ b/HaxeSwiftBridge.hx @@ -549,6 +549,10 @@ class HaxeSwiftBridge { builder.add(", "); builder.add(iface.t.get().name); } + final asyncSequence = cls.meta.extract(":HaxeSwiftBridge.asyncSequence").find(_ -> true)?.params?.map(identToStr); + if (asyncSequence != null) { + builder.add(", AsyncIteratorProtocol, AsyncSequence"); + } if (!cls.isInterface) { builder.add(", @unchecked Sendable"); } @@ -563,6 +567,13 @@ class HaxeSwiftBridge { builder.add("\n\tinternal init(_ ptr: UnsafeMutableRawPointer) {\n\t\to = ptr\n\t\tc_borogove.borogove_set_finalizer(ptr, releaseContexts)\n\t}\n\n"); } + + if (asyncSequence != null) { + builder.add("\tpublic typealias Element = " + asyncSequence[0] + "\n"); + builder.add("\tpublic typealias AsyncIterator = AvailableChatIterator\n"); + builder.add("\tpublic func makeAsyncIterator() -> AvailableChatIterator { return self }\n\n"); + } + if (!cls.isInterface && superClass != null) { builder.add("\tinternal override init(_ ptr: UnsafeMutableRawPointer) {\n\t\tsuper.init(ptr)\n\t}\n\n"); } diff --git a/borogove/AvailableChatIterator.hx b/borogove/AvailableChatIterator.hx new file mode 100644 index 0000000..1bf1001 --- /dev/null +++ b/borogove/AvailableChatIterator.hx @@ -0,0 +1,166 @@ +package borogove; + +import thenshim.Promise; + +import borogove.Chat; +import borogove.queries.DiscoInfoGet; +import borogove.queries.JabberIqGatewayGet; +import borogove.Util; +using Lambda; +using StringTools; + +#if cpp +import HaxeCBridge; +#end + +@:expose +#if cpp +@:build(HaxeCBridge.expose()) +@:build(HaxeSwiftBridge.expose()) +@:HaxeSwiftBridge.asyncSequence(AvailableChat) +#end +class AvailableChatIterator { + /** + The query that this iterator is returning results for + **/ + public final q: String; + private final query: String; + private final client: Client; + private final persistence: Persistence; + private var results: Array<Promise<Null<AvailableChat>>> = []; + private var dedup: Map<String, Bool> = []; + + @:allow(borogove) + private function new(q: String, client: Client, persistence: Persistence) { + this.q = q; + this.client = client; + this.persistence = persistence; + this.query = q.trim(); + + final vcard_regex = ~/\nIMPP[^:]*:xmpp:(.+)\n/; + final jid = if (StringTools.startsWith(query, "xmpp:")) { + final parts = query.substr(5).split("?"); + JID.parse(uriDecode(parts[0])); + } else if (StringTools.startsWith(query, "BEGIN:VCARD") && vcard_regex.match(query)) { + final parts = vcard_regex.matched(1).split("?"); + JID.parse(uriDecode(parts[0])); + } else if (StringTools.startsWith(query, "https://")) { + final hashParts = query.split("#"); + if (hashParts.length > 1) { + JID.parse(uriDecode(hashParts[1])); + } else { + final pathParts = hashParts[0].split("/"); + JID.parse(uriDecode(pathParts[pathParts.length - 1])); + } + } else { + JID.parse(query); + } + if (jid.isValid()) { + results.push(check(jid)); + } + + if (StringTools.startsWith(query, "https://")) { + results.push(xmppLinkHeader(query).then(xmppUri -> { + final parts = xmppUri.substr(5).split("?"); + final jid = JID.parse(uriDecode(parts[0])); + if (jid.isValid()) return check(jid); + + return Promise.resolve(null); + })); + } + + for (chat in client.chats) { + if (chat.chatId != client.accountId()) { + if (chat.chatId.contains(query.toLowerCase()) || chat.getDisplayName().toLowerCase().contains(query.toLowerCase())) { + final channel = Util.downcast(chat, Channel); + results.push(Promise.resolve(new AvailableChat(chat.chatId, chat.getDisplayName(), chat.chatId, channel == null || channel.disco == null ? new Caps("", [], [], []) : channel.disco))); + } + } + if (chat.isTrusted()) { + final resources:Map<String, Bool> = []; + for (resource in Caps.withIdentity(chat.getCaps(), "gateway", null)) { + // Sometimes gateway items also have id "gateway" for whatever reason + final identities = chat.getResourceCaps(resource)?.identities ?? []; + if ( + (chat.chatId.indexOf("@") < 0 || identities.find(i -> i.category == "conference") == null) && + identities.find(i -> i.category == "client") == null + ) { + resources[resource] = true; + } + } + /* Gajim advertises this, so just go with identity instead + for (resource in Caps.withFeature(chat.getCaps(), "jabber:iq:gateway")) { + resources[resource] = true; + }*/ + if (!client.sendAvailable && JID.parse(chat.chatId).isDomain()) { + resources[null] = true; + } + for (resource in resources.keys()) { + final bareJid = JID.parse(chat.chatId); + final fullJid = new JID(bareJid.node, bareJid.domain, bareJid.isDomain() && resource == "" ? null : resource); + final jigGet = new JabberIqGatewayGet(fullJid.asString(), query); + results.push(new Promise((resolve, reject) -> { + jigGet.onFinished(() -> { + final result = jigGet.getResult(); + if (result == null) { + final caps = chat.getResourceCaps(resource); + if (bareJid.isDomain() && caps.features.contains("jid\\20escaping")) { + check(new JID(query, bareJid.domain)).then(resolve); + } else if (bareJid.isDomain()) { + check(new JID(StringTools.replace(query, "@", "%"), bareJid.domain)).then(resolve); + } + } else { + switch (result) { + case Left(error): resolve(null); + case Right(result): + check(JID.parse(result)).then(resolve); + } + } + }); + client.sendQuery(jigGet); + })); + } + } + } + } + + private function check(jid: JID) { + return new Promise((resolve, reject) -> { + final discoGet = new DiscoInfoGet(jid.asString()); + discoGet.onFinished(() -> { + final resultCaps = discoGet.getResult(); + if (resultCaps == null) { + final err = discoGet.responseStanza?.getChild("error")?.getChild(null, "urn:ietf:params:xml:ns:xmpp-stanzas"); + if (err == null || err?.name == "service-unavailable" || err?.name == "feature-not-implemented") { + resolve(new AvailableChat(jid.asString(), jid.node == null ? query : jid.node, jid.asString(), new Caps("", [], [], []))); + } else { + resolve(null); + } + } else { + persistence.storeCaps(resultCaps); + final identity = resultCaps.identities[0]; + final displayName = identity?.name ?? query; + final note = jid.asString() + (identity == null ? "" : " (" + identity.type + ")"); + resolve(new AvailableChat(jid.asString(), displayName, note, resultCaps)); + } + }); + client.sendQuery(discoGet); + }); + } + + /** + Get the next AvailableChat from this iterator + **/ + public function next(): Promise<Null<AvailableChat>> { + if (results.length < 1) return Promise.resolve(null); + + return results.shift().then(available -> { + if (available == null || dedup[available.chatId]) { + return this.next(); + } else { + dedup[available.chatId] = true; + return Promise.resolve(available); + } + }); + } +} diff --git a/borogove/Client.hx b/borogove/Client.hx index 582a50f..b0c2ada 100644 --- a/borogove/Client.hx +++ b/borogove/Client.hx @@ -6,6 +6,7 @@ import haxe.crypto.Base64; import haxe.io.Bytes; import haxe.io.BytesData; import thenshim.Promise; +import borogove.AvailableChatIterator; import borogove.Caps; import borogove.Chat; import borogove.ChatMessage; @@ -31,7 +32,6 @@ import borogove.queries.DiscoItemsGet; import borogove.queries.ExtDiscoGet; import borogove.queries.GenericQuery; import borogove.queries.HttpUploadSlot; -import borogove.queries.JabberIqGatewayGet; import borogove.queries.PubsubGet; import borogove.queries.Push2Disable; import borogove.queries.Push2Enable; @@ -60,10 +60,12 @@ class Client extends EventEmitter { /** Set to false to suppress sending available presence **/ + @:allow(borogove) public var sendAvailable(null, default): Bool = true; private var stream:GenericStream; @:allow(borogove) private var jid(default,null):JID; + @:allow(borogove) private var chats: Array<Chat> = []; private var persistence: Persistence; private final caps = new Caps( @@ -947,119 +949,10 @@ class Client extends EventEmitter { Search for chats the user can start or join @param q the search query to use - @param callback takes two arguments, the query that was used and the array of results, and returns true if we should stop searching + @returns an async iterator of AvailableChat matching the query **/ - public function findAvailableChats(q:String, callback:(String, Array<AvailableChat>) -> Bool) { - var haveJid: Map<String, Bool> = []; - var results = []; - final query = StringTools.trim(q); - final checkAndAdd = (jid: JID, prepend = false) -> { - if (haveJid[jid.asString()]) return; - haveJid[jid.asString()] = true; - - final add = (item) -> prepend ? results.unshift(item) : results.push(item); - final discoGet = new DiscoInfoGet(jid.asString()); - discoGet.onFinished(() -> { - final resultCaps = discoGet.getResult(); - if (resultCaps == null) { - final err = discoGet.responseStanza?.getChild("error")?.getChild(null, "urn:ietf:params:xml:ns:xmpp-stanzas"); - if (err == null || err?.name == "service-unavailable" || err?.name == "feature-not-implemented") { - add(new AvailableChat(jid.asString(), jid.node == null ? query : jid.node, jid.asString(), new Caps("", [], [], []))); - } - } else { - persistence.storeCaps(resultCaps); - final identity = resultCaps.identities[0]; - final displayName = identity?.name ?? query; - final note = jid.asString() + (identity == null ? "" : " (" + identity.type + ")"); - add(new AvailableChat(jid.asString(), displayName, note, resultCaps)); - } - if (callback != null && callback(q, results)) callback = null; - }); - sendQuery(discoGet); - }; - final vcard_regex = ~/\nIMPP[^:]*:xmpp:(.+)\n/; - final jid = if (StringTools.startsWith(query, "xmpp:")) { - final parts = query.substr(5).split("?"); - JID.parse(uriDecode(parts[0])); - } else if (StringTools.startsWith(query, "BEGIN:VCARD") && vcard_regex.match(query)) { - final parts = vcard_regex.matched(1).split("?"); - JID.parse(uriDecode(parts[0])); - } else if (StringTools.startsWith(query, "https://")) { - final hashParts = query.split("#"); - if (hashParts.length > 1) { - JID.parse(uriDecode(hashParts[1])); - } else { - final pathParts = hashParts[0].split("/"); - JID.parse(uriDecode(pathParts[pathParts.length - 1])); - } - } else { - JID.parse(query); - } - if (jid.isValid()) { - checkAndAdd(jid, true); - } - - if (StringTools.startsWith(query, "https://")) { - xmppLinkHeader(query).then(xmppUri -> { - final parts = xmppUri.substr(5).split("?"); - final jid = JID.parse(uriDecode(parts[0])); - if (jid.isValid()) checkAndAdd(jid, true); - }); - } - - for (chat in chats) { - if (chat.chatId != jid.asBare().asString()) { - if (chat.chatId.contains(query.toLowerCase()) || chat.getDisplayName().toLowerCase().contains(query.toLowerCase())) { - final channel = Util.downcast(chat, Channel); - results.push(new AvailableChat(chat.chatId, chat.getDisplayName(), chat.chatId, channel == null || channel.disco == null ? new Caps("", [], [], []) : channel.disco)); - } - } - if (chat.isTrusted()) { - final resources:Map<String, Bool> = []; - for (resource in Caps.withIdentity(chat.getCaps(), "gateway", null)) { - // Sometimes gateway items also have id "gateway" for whatever reason - final identities = chat.getResourceCaps(resource)?.identities ?? []; - if ( - (chat.chatId.indexOf("@") < 0 || identities.find(i -> i.category == "conference") == null) && - identities.find(i -> i.category == "client") == null - ) { - resources[resource] = true; - } - } - /* Gajim advertises this, so just go with identity instead - for (resource in Caps.withFeature(chat.getCaps(), "jabber:iq:gateway")) { - resources[resource] = true; - }*/ - if (!sendAvailable && JID.parse(chat.chatId).isDomain()) { - resources[null] = true; - } - for (resource in resources.keys()) { - final bareJid = JID.parse(chat.chatId); - final fullJid = new JID(bareJid.node, bareJid.domain, bareJid.isDomain() && resource == "" ? null : resource); - final jigGet = new JabberIqGatewayGet(fullJid.asString(), query); - jigGet.onFinished(() -> { - if (jigGet.getResult() == null) { - final caps = chat.getResourceCaps(resource); - if (bareJid.isDomain() && caps.features.contains("jid\\20escaping")) { - checkAndAdd(new JID(query, bareJid.domain)); - } else if (bareJid.isDomain()) { - checkAndAdd(new JID(StringTools.replace(query, "@", "%"), bareJid.domain)); - } - } else { - switch (jigGet.getResult()) { - case Left(error): return; - case Right(result): - checkAndAdd(JID.parse(result)); - } - } - }); - sendQuery(jigGet); - } - } - } - if (!jid.isValid() && results.length > 0) { - if (callback != null && callback(q, results)) callback = null; - } + public function findAvailableChats(q:String): AvailableChatIterator { + return new AvailableChatIterator(q, this, persistence); } /**