git » sdk » main » tree

[main] / borogove / Caps.hx

package borogove;

import haxe.crypto.Base64;
import haxe.ds.ReadOnlyArray;
import haxe.io.Bytes;
import haxe.io.BytesData;
using Lambda;
using borogove.Util;

import borogove.DataForm;
import borogove.Hash;
import borogove.Util;

class Caps {
	public final node: String;
	public final identities: ReadOnlyArray<Identity>;
	public final features : ReadOnlyArray<String>;
	public final data: ReadOnlyArray<DataForm>;
	private var _ver : Null<Hash> = null;

	@:allow(borogove)
	private static function withIdentity(caps:KeyValueIterator<String, Null<Caps>>, category:Null<String>, type:Null<String>):Array<String> {
		final result = [];
		for (cap in caps) {
			if (cap.value != null) {
				for (identity in cap.value.identities) {
					if ((category == null || category == identity.category) && (type == null || type == identity.type)) {
						result.push(cap.key);
					}
				}
			}
		}
		return result;
	}

	@:allow(borogove)
	private static function withFeature(caps:KeyValueIterator<String, Null<Caps>>, feature:String):Array<String> {
		final result = [];
		for (cap in caps) {
			if (cap.value != null) {
				for (feat in cap.value.features) {
					if (feature == feat) {
						result.push(cap.key);
					}
				}
			}
		}
		return result;
	}

	public function new(node: String, identities: Array<Identity>, features: Array<String>, data: Array<DataForm>, ?ver: BytesData) {
		if (ver == null) {
			// If we won't need to generate ver we don't actually need to sort
			features.sort((x, y) -> x == y ? 0 : (x < y ? -1 : 1));
			identities.sort((x, y) -> x.ver() == y.ver() ? 0 : (x.ver() < y.ver() ? -1 : 1));
			data.sort((x, y) -> Reflect.compare(x.field("FORM_TYPE")?.value, y.field("FORM_TYPE")?.value));
		}

		this.node = node;
		this.identities = identities;
		this.features = features;
		this.data = data;
		if (ver != null) {
			_ver = new Hash("sha-1", ver);
		}
	}

	public function isChannel(chatId: String) {
		if (chatId.indexOf("@") < 0) return false; // MUC must have a localpart
		return features.contains("http://jabber.org/protocol/muc") && identities.find((identity) -> identity.category == "conference") != null;
	}

	public function discoReply():Stanza {
		final query = new Stanza("query", { xmlns: "http://jabber.org/protocol/disco#info" });
		for (identity in identities) {
			identity.addToDisco(query);
		}
		for (feature in features) {
			query.tag("feature", { "var": feature }).up();
		}
		query.addChildren(data);
		return query;
	}

	public function addC(stanza: Stanza): Stanza {
		stanza.tag("c", {
			xmlns: "http://jabber.org/protocol/caps",
			hash: "sha-1",
			node: node,
			ver: ver()
		}).up();
		stanza.tag("c", {
			xmlns: "urn:xmpp:caps",
		}).textTag(
			"hash",
			Hash.sha256(hashInput()).toBase64(),
			{ xmlns: "urn:xmpp:hashes:2", algo: "sha-256" }
		).up();
		return stanza;
	}

	private function hashInput(): Bytes {
		var s = new haxe.io.BytesOutput();
		for (feature in features) {
			s.writeS(feature);
			s.writeByte(0x1f);
		}
		s.writeByte(0x1c);
		for (identity in identities) {
			identity.writeTo(s);
		}
		s.writeByte(0x1c);
		for (form in data) {
			final fields = form.fields();
			fields.sort((x, y) -> Reflect.compare([x.name].concat(x.value).join("\x1f"), [y.name].concat(y.value).join("\x1f")));
			for (field in fields) {
				final values = field.value;
				values.sort(Reflect.compare);
				s.writeS(field.name);
				s.writeByte(0x1f);
				for (value in values) {
					s.writeS(value);
					s.writeByte(0x1f);
				}
				s.writeByte(0x1e);
			}
			s.writeByte(0x1d);
		}
		s.writeByte(0x1c);
		return s.getBytes();
	}

	private function computeVer(): Hash {
		var s = "";
		for (identity in identities) {
			s += identity.ver() + "<";
		}
		for (feature in features) {
			s += feature + "<";
		}
		for (form in data) {
			s += form.field("FORM_TYPE").value[0] + "<";
			final fields = form.fields();
			fields.sort((x, y) -> Reflect.compare(x.name, y.name));
			for (field in fields) {
				if (field.name != "FORM_TYPE") {
					s += field.name + "<";
					final values = field.value;
					values.sort(Reflect.compare);
					for (value in values) {
						s += value + "<";
					}
				}
			}
		}
		return Hash.sha1(bytesOfString(s));
	}

	public function verRaw(): Hash {
		if (_ver == null) _ver = computeVer();
		return _ver;
	}

	public function ver(): String {
		return verRaw().toBase64();
	}
}

class Identity {
	public final category:String;
	public final type:String;
	public final name:String;
	public final lang:String;

	public function new(category:String, type: String, name: String, lang: Null<String> = null) {
		this.category = category;
		this.type = type;
		this.name = name;
		this.lang = lang ?? "";
	}

	public function addToDisco(stanza: Stanza) {
		var attrs: haxe.DynamicAccess<String> = { category: category, type: type, name: name };
		if (lang != null && lang != "") attrs.set("xml:lang", lang);
		stanza.tag("identity", attrs).up();
	}

	public function ver(): String {
		return category + "/" + type + "/" + (lang ?? "") + "/" + name;
	}

	public function writeTo(out: haxe.io.Output) {
		out.writeS(category);
		out.writeByte(0x1f);
		out.writeS(type);
		out.writeByte(0x1f);
		out.writeS(lang ?? "");
		out.writeByte(0x1f);
		out.writeS(name);
		out.writeByte(0x1f);
		out.writeByte(0x1e);
	}
}