git » sdk » main » tree

[main] / borogove / AvailableChatIterator.hx

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
	**/
	#if js
	@:native("[Symbol.asyncIterator]")
	public function asyncIterator() {
		return this;
	}

	public function next(): Promise<{ done: Bool, ?value: AvailableChat }> {
		return internalNext().then(v -> {
			return { done: v == null, value: v };
		});
	}
	#else
	public function next(): Promise<Null<AvailableChat>> {
		return internalNext();
	}
	#end

	private function internalNext(): Promise<Null<AvailableChat>> {
		if (results.length < 1) return Promise.resolve(null);

		return results.shift().then(available -> {
			if (available == null || dedup[available.chatId]) {
				return this.internalNext();
			} else {
				dedup[available.chatId] = true;
				return Promise.resolve(available);
			}
		});
	}
}