git » sdk » commit 1e511c0

Fetch and cache caps

author Stephen Paul Weber
2023-09-14 01:53:49 UTC
committer Stephen Paul Weber
2023-09-25 17:09:48 UTC
parent 65f024d3a34d55593fda711eaf9e21597b76ebba

Fetch and cache caps

xmpp/Caps.hx +104 -0
xmpp/Chat.hx +5 -0
xmpp/Client.hx +26 -0
xmpp/Persistence.hx +2 -0
xmpp/persistence/browser.js +13 -0
xmpp/queries/DiscoInfoGet.hx +55 -0

diff --git a/xmpp/Caps.hx b/xmpp/Caps.hx
new file mode 100644
index 0000000..35828a4
--- /dev/null
+++ b/xmpp/Caps.hx
@@ -0,0 +1,104 @@
+package xmpp;
+
+import haxe.crypto.Base64;
+import haxe.crypto.Sha1;
+import haxe.io.Bytes;
+
+@:expose
+class Caps {
+	private final node: String;
+	private final identities: Array<Identity>;
+	public final features : Array<String>;
+	// TODO: data forms
+
+	public static function withIdentity(caps:KeyValueIterator<String, Caps>, category:Null<String>, type:Null<String>):Array<String> {
+		final result = [];
+		for (cap in caps) {
+			for (identity in cap.value.identities) {
+				if ((category == null || category == identity.category) && (type == null || type == identity.type)) {
+					result.push(cap.key);
+				}
+			}
+		}
+		return result;
+	}
+
+	public static function withFeature(caps:KeyValueIterator<String, Caps>, feature:String):Array<String> {
+		final result = [];
+		for (cap in caps) {
+			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>) {
+		this.node = node;
+		this.identities = identities;
+		this.features = features;
+	}
+
+	public function discoReply(stanza: Stanza):Stanza {
+		final reply = new Stanza("iq", {
+			type: "result",
+			id: stanza.attr.get("id"),
+			to: stanza.attr.get("from")
+		});
+		final query = reply.tag("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.up();
+		return reply;
+	}
+
+	public function addC(stanza: Stanza): Stanza {
+		stanza.tag("c", {
+			xmlns: "http://jabber.org/protocol/caps",
+			hash: "sha-1",
+			node: node,
+			ver: ver()
+		}).up();
+		return stanza;
+	}
+
+	public function ver(): String {
+		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));
+		var s = "";
+		for (identity in identities) {
+			s += identity.ver() + "<";
+		}
+		for (feature in features) {
+			s += feature + "<";
+		}
+		return Base64.encode(Sha1.make(Bytes.ofString(s)), false);
+	}
+}
+
+@:expose
+class Identity {
+	public final category:String;
+	public final type:String;
+	public final name:String;
+
+	public function new(category:String, type: String, name: String) {
+		this.category = category;
+		this.type = type;
+		this.name = name;
+	}
+
+	public function addToDisco(stanza: Stanza) {
+		stanza.tag("identity", { category: category, type: type, name: name }).up();
+	}
+
+	public function ver(): String {
+		return category + "/" + type + "//" + name;
+	}
+}
diff --git a/xmpp/Chat.hx b/xmpp/Chat.hx
index e9c8d6b..2faf35f 100644
--- a/xmpp/Chat.hx
+++ b/xmpp/Chat.hx
@@ -20,6 +20,7 @@ abstract class Chat {
 	private var stream:GenericStream;
 	private var persistence:Persistence;
 	private var avatarSha1:Null<BytesData> = null;
+	private var caps:Map<String, Caps> = [];
 	public var chatId(default, null):String;
 	public var type(default, null):Null<ChatType>;
 
@@ -40,6 +41,10 @@ abstract class Chat {
 	public function isGroupChat():Bool  { return type.match(ChatTypeGroup);  };
 	public function isPublicChat():Bool { return type.match(ChatTypePublic); };
 
+	public function setCaps(resource:String, caps:Caps) {
+		this.caps.set(resource, caps);
+	}
+
 	public function onMessage(handler:ChatMessage->Void):Void {
 		this.stream.on("message", function(event) {
 			final stanza:Stanza = event.stanza;
diff --git a/xmpp/Client.hx b/xmpp/Client.hx
index cd67c71..2ac1503 100644
--- a/xmpp/Client.hx
+++ b/xmpp/Client.hx
@@ -10,6 +10,7 @@ import xmpp.Stream;
 import xmpp.queries.GenericQuery;
 import xmpp.queries.RosterGet;
 import xmpp.queries.PubsubGet;
+import xmpp.queries.DiscoInfoGet;
 import xmpp.PubsubEvent;
 
 typedef ChatList = Array<Chat>;
@@ -124,6 +125,31 @@ class Client extends xmpp.EventEmitter {
 			return EventHandled;
 		});
 
+		this.stream.on("presence", function(event) {
+			final stanza:Stanza = event.stanza;
+			final c = stanza.getChild("c", "http://jabber.org/protocol/caps");
+			if (c != null && stanza.attr.get("from") != null) {
+				final chat = getDirectChat(stanza.attr.get("from"), false);
+				persistence.getCaps(c.attr.get("ver"), (caps) -> {
+					if (caps == null) {
+						final discoGet = new DiscoInfoGet(stanza.attr.get("from"), c.attr.get("node") + "#" + c.attr.get("ver"));
+						discoGet.onFinished(() -> {
+							if (discoGet.getResult() != null) {
+								persistence.storeCaps(discoGet.getResult());
+								chat.setCaps(JID.parse(stanza.attr.get("from")).resource, discoGet.getResult());
+							}
+						});
+						sendQuery(discoGet);
+					} else {
+						chat.setCaps(JID.parse(stanza.attr.get("from")).resource, caps);
+					}
+				});
+				return EventHandled;
+			}
+
+			return EventUnhandled;
+		});
+
 		// Set self to online
 		stream.sendStanza(caps.addC(new Stanza("presence")));
 
diff --git a/xmpp/Persistence.hx b/xmpp/Persistence.hx
index 3698acb..39b2cbe 100644
--- a/xmpp/Persistence.hx
+++ b/xmpp/Persistence.hx
@@ -9,4 +9,6 @@ abstract class Persistence {
 	abstract public function getMessages(accountId: String, chatId: String, beforeId: Null<String>, beforeTime: Null<String>, callback: (messages:Array<ChatMessage>)->Void):Void;
 	abstract public function getMediaUri(hashAlgorithm:String, hash:BytesData, callback: (uri:Null<String>)->Void):Void;
 	abstract public function storeMedia(mime:String, bytes:BytesData, callback: ()->Void):Void;
+	abstract public function storeCaps(caps:Caps):Void;
+	abstract public function getCaps(ver:String, callback: (Caps)->Void):Void;
 }
diff --git a/xmpp/persistence/browser.js b/xmpp/persistence/browser.js
index 0848e4d..9a32d34 100644
--- a/xmpp/persistence/browser.js
+++ b/xmpp/persistence/browser.js
@@ -133,6 +133,19 @@ exports.xmpp.persistence = {
 					localStorage.setItem(mkNiUrl("sha-1", sha1), sha256NiUrl);
 					localStorage.setItem(mkNiUrl("sha-512", sha512), sha256NiUrl);
 				})().then(callback);
+			},
+
+			storeCaps: function(caps) {
+				localStorage.setItem("caps:" + caps.ver(), JSON.stringify(caps));
+			},
+
+			getCaps: function(ver, callback) {
+				const raw = JSON.parse(localStorage.getItem("caps:" + ver));
+				if (raw) {
+					callback(new xmpp.Caps(raw.node, raw.identities.map((identity) => new xmpp.Identity(identity.category, identity.type, identity.name)), raw.features));
+				} else {
+					callback(null);
+				}
 			}
 		}
 	}
diff --git a/xmpp/queries/DiscoInfoGet.hx b/xmpp/queries/DiscoInfoGet.hx
new file mode 100644
index 0000000..6fdfab1
--- /dev/null
+++ b/xmpp/queries/DiscoInfoGet.hx
@@ -0,0 +1,55 @@
+package xmpp.queries;
+
+import haxe.DynamicAccess;
+import haxe.Exception;
+
+import xmpp.ID;
+import xmpp.ResultSet;
+import xmpp.Stanza;
+import xmpp.Stream;
+import xmpp.queries.GenericQuery;
+import xmpp.Caps;
+
+class DiscoInfoGet extends GenericQuery {
+	public var xmlns(default, null) = "http://jabber.org/protocol/disco#info";
+	public var queryId:String = null;
+	public var ver:String = null;
+	private var responseStanza:Stanza;
+	private var result: Caps;
+
+	public function new(to: String, ?node: String) {
+		var attr: DynamicAccess<String> = { xmlns: xmlns };
+		if (node != null) attr["node"] = node;
+		/* Build basic query */
+		queryId = ID.short();
+		queryStanza = new Stanza(
+			"iq",
+			{ to: to, type: "get", id: queryId }
+		).tag("query", attr).up();
+	}
+
+	public function handleResponse(stanza:Stanza) {
+		responseStanza = stanza;
+		finish();
+	}
+
+	public function getResult() {
+		if (responseStanza == null) {
+			return null;
+		}
+		if(result == null) {
+			final q = responseStanza.getChild("query", xmlns);
+			if(q == null) {
+				return null;
+			}
+			final identities = q.allTags("identity");
+			final features = q.allTags("feature");
+			result = new Caps(
+				q.attr.get("node"),
+				identities.map((identity) -> new Identity(identity.attr.get("category"), identity.attr.get("type"), identity.attr.get("name"))),
+				features.map((feature) -> feature.attr.get("var"))
+			);
+		}
+		return result;
+	}
+}