| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2023-09-14 01:53:49 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2023-09-25 17:09:48 UTC |
| parent | 65f024d3a34d55593fda711eaf9e21597b76ebba |
| 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; + } +}