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;
// exposed for IDB.js, not really part of public API
@:expose
@:nullSafety(StrictThreaded)
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;
private static function filter(caps:KeyValueIterator<Null<String>, Caps>, predicate:Caps->Bool):KeyValueIterator<Null<String>, Caps> {
var nextMatch:Null<{key:Null<String>, value:Caps}> = null;
final findNext = () -> {
while (caps.hasNext()) {
var n = caps.next();
if (predicate(n.value)) {
nextMatch = n;
return;
}
}
nextMatch = null;
};
findNext();
return {
hasNext: () -> nextMatch != null,
next: () -> {
final r = nextMatch;
if (r == null) throw "No more elements";
findNext();
return r;
}
};
}
@:allow(borogove)
private static function withIdentity(caps:KeyValueIterator<Null<String>, Caps>, category:Null<String>, type:Null<String>):KeyValueIterator<Null<String>, Caps> {
return filter(caps, (c) -> c.identities.exists((identity) -> (category == null || category == identity.category) && (type == null || type == identity.type)));
}
@:allow(borogove)
private static function withFeature(caps:KeyValueIterator<Null<String>, Caps>, feature:String):KeyValueIterator<Null<String>, Caps> {
return filter(caps, (c) -> c.features.contains(feature));
}
/**
Create a capabilities description.
@param node capability node identifier
@param identities disco identities advertised by the entity
@param features disco feature namespaces advertised by the entity
@param data extended disco data forms
@param ver optional precomputed capability hash bytes
**/
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 ?? []).join("\n"), (y.field("FORM_TYPE")?.value ?? []).join("\n")));
}
this.node = node;
this.identities = identities;
this.features = features;
this.data = data;
if (ver != null) {
_ver = new Hash("sha-1", ver);
}
}
/**
Check whether these capabilities describe a channel-like chat target.
@param chatId ID to evaluate against the capability set
@returns true when the target looks like a MUC/channel
**/
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;
}
/**
Build a disco#info query payload for this capability set.
**/
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;
}
/**
Add capability advertisements to a stanza.
@param stanza stanza to mutate
@returns the same stanza for chaining
**/
public function addC(stanza: Stanza): Stanza {
stanza.tag("c", {
xmlns: "http://jabber.org/protocol/caps",
hash: "sha-1",
node: node,
ver: ver()
}).up();
if (identities.length > 0 || features.length > 0 || data.length > 0) {
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) {
final formType = form.field("FORM_TYPE");
s += formType == null ? "" : formType.value[0];
s += "<";
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));
}
/**
Get the raw XEP-0115 capability hash object for this capability set.
**/
public function verRaw(): Hash {
final ver = _ver;
if (ver != null) return ver;
final newVer = computeVer();
_ver = newVer;
return newVer;
}
/**
Get the XEP-0115 capability hash encoded in base64.
**/
public function ver(): String {
return verRaw().toBase64();
}
}
@:expose
class Identity {
public final category:String;
public final type:String;
public final name:String;
public final lang:String;
/**
Create a disco identity.
**/
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 ?? "";
}
/**
Add this identity to a disco#info payload.
**/
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();
}
/**
Get the identity string used when computing capability hashes.
**/
public function ver(): String {
return category + "/" + type + "/" + (lang ?? "") + "/" + name;
}
/**
Write the identity in canonical capability-hash form.
**/
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);
}
}