| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2025-11-11 15:29:03 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2025-11-11 15:29:03 UTC |
| parent | 797c73b39630058f1cbe5993fa43055ba8d4fd41 |
| borogove/Caps.hx | +2 | -2 |
| borogove/Chat.hx | +1 | -0 |
| borogove/Client.hx | +10 | -20 |
| borogove/DataForm.hx | +129 | -5 |
| borogove/Form.hx | +208 | -0 |
| borogove/GenericStream.hx | +3 | -1 |
| borogove/Register.hx | +149 | -0 |
| borogove/Util.hx | +58 | -0 |
| borogove/streams/XmppJsStream.hx | +105 | -4 |
| borogove/streams/XmppStropheStream.hx | +5 | -0 |
| browserjs.hxml | +1 | -0 |
| nodejs.hxml | +1 | -0 |
| npm/index.ts | +4 | -0 |
diff --git a/borogove/Caps.hx b/borogove/Caps.hx index db52fdf..6b1ff29 100644 --- a/borogove/Caps.hx +++ b/borogove/Caps.hx @@ -113,7 +113,7 @@ class Caps { } s.writeByte(0x1c); for (form in data) { - final fields = form.fields(); + 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; @@ -142,7 +142,7 @@ class Caps { } for (form in data) { s += form.field("FORM_TYPE").value[0] + "<"; - final fields = form.fields(); + final fields = form.fields; fields.sort((x, y) -> Reflect.compare(x.name, y.name)); for (field in fields) { if (field.name != "FORM_TYPE") { diff --git a/borogove/Chat.hx b/borogove/Chat.hx index 90067cb..a769530 100644 --- a/borogove/Chat.hx +++ b/borogove/Chat.hx @@ -20,6 +20,7 @@ import borogove.queries.DiscoInfoGet; import borogove.queries.MAMQuery; using Lambda; using StringTools; +using borogove.Util; #if cpp import HaxeCBridge; diff --git a/borogove/Client.hx b/borogove/Client.hx index 9503dec..dc66671 100644 --- a/borogove/Client.hx +++ b/borogove/Client.hx @@ -17,6 +17,7 @@ import borogove.OMEMO; #end import borogove.PubsubEvent; import borogove.Stream; +import borogove.Util; #if !NO_JINGLE import borogove.calls.IceServer; import borogove.calls.PeerConnection; @@ -911,17 +912,17 @@ class Client extends EventEmitter { final vcard_regex = ~/\nIMPP[^:]*:xmpp:(.+)\n/; final jid = if (StringTools.startsWith(query, "xmpp:")) { final parts = query.substr(5).split("?"); - JID.parse(StringTools.urlDecode(parts[0])); + 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(StringTools.urlDecode(parts[0])); + JID.parse(uriDecode(parts[0])); } else if (StringTools.startsWith(query, "https://")) { final hashParts = query.split("#"); if (hashParts.length > 1) { - JID.parse(StringTools.urlDecode(hashParts[1])); + JID.parse(uriDecode(hashParts[1])); } else { final pathParts = hashParts[0].split("/"); - JID.parse(StringTools.urlDecode(pathParts[pathParts.length - 1])); + JID.parse(uriDecode(pathParts[pathParts.length - 1])); } } else { JID.parse(query); @@ -931,22 +932,11 @@ class Client extends EventEmitter { } if (StringTools.startsWith(query, "https://")) { - tink.http.Client.fetch(query, { - method: HEAD - }).all() - .handle(function(o) switch o { - case Success(res): - final regex = ~/<xmpp:([^>]+)>/; - for (link in res.header.get("link")) { - if (regex.match(link)) { - final parts = regex.matched(1).split("?"); - final jid = JID.parse(StringTools.urlDecode(parts[0])); - if (jid.isValid()) checkAndAdd(jid, true); - } - } - case Failure(e): - trace("findAvailable request failed", e); - }); + xmppLinkHeader(query).then(xmppUri -> { + final parts = xmppUri.substr(5).split("?"); + final jid = JID.parse(uriDecode(parts[0])); + if (jid.isValid()) checkAndAdd(jid, true); + }); } for (chat in chats) { diff --git a/borogove/DataForm.hx b/borogove/DataForm.hx index ffad925..f4af46a 100644 --- a/borogove/DataForm.hx +++ b/borogove/DataForm.hx @@ -2,14 +2,25 @@ package borogove; import borogove.Stanza; +#if cpp +import HaxeCBridge; +#end + @:forward(toString) abstract DataForm(Stanza) from Stanza to Stanza { - inline public function fields(): Array<Field> { + public var title(get, never): Null<String>; + public var fields(get, never): Array<Field>; + + inline public function get_title() { + return this.getChildText("title"); + } + + inline public function get_fields() { return this.allTags("field"); } public function field(name: String): Null<Field> { - final matches = fields().filter(f -> f.name == name); + final matches = fields.filter(f -> f.name == name); if (matches.length > 1) { trace('Multiple fields matching ${name}'); } @@ -19,13 +30,126 @@ abstract DataForm(Stanza) from Stanza to Stanza { abstract Field(Stanza) from Stanza to Stanza { public var name(get, never): String; - public var value(get, never): Array<String>; + public var label(get, never): Null<String>; + public var value(get, set): Array<String>; + public var type(get, never): String; + public var datatype(get, never): String; + public var open(get, never): Bool; + public var rangeMin(get, never): Null<String>; + public var rangeMax(get, never): Null<String>; + public var regex(get, never): Null<String>; + public var required(get, never): Bool; inline public function get_name() { - return this.attr.get("var"); + return this.attr.get("var") ?? ""; + } + + inline public function get_label() { + return this.attr.get("label"); } public function get_value() { - return this.allTags("value").map(v -> v.getText()); + final isbool = (this : Field).datatype == "xs:boolean"; + return this.allTags("value").map(v -> { + final txt = v.getText(); + if (isbool) { + Stanza.parseXmlBool(txt) ? "true" : "false"; + } else { + return txt; + } + }); + } + + public function set_value(val: Array<String>) { + this.removeChildren("value"); + for (v in val) { + this.textTag("value", v); + } + return val; + } + + inline public function get_type() { + final attr = this.attr.get("type"); + + // We will treat jid as a datatype not a field type + if (attr == "jid-single") return "text-single"; + if (attr == "jid-multi") return "text-multi"; + return attr; + } + + public function get_datatype() { + final validate = this.getChild("validate", "http://jabber.org/protocol/xdata-validate"); + if (validate != null && validate.attr.get("datatype") != null) { + return validate.attr.get("datatype"); + } + if (["jid-single", "jid-multi"].contains(this.attr.get("type"))) { + return "jid"; + } + if (this.attr.get("type") == "boolean") return "xs:boolean"; + return "xs:string"; + } + + inline public function get_open() { + final validate = this.getChild("validate", "http://jabber.org/protocol/xdata-validate"); + return validate?.getChild("open") != null; + } + + inline public function get_rangeMin() { + return range()?.attr?.get("min"); + } + + inline public function get_rangeMax() { + return range()?.attr?.get("max"); + } + + inline private function range() { + final validate = this.getChild("validate", "http://jabber.org/protocol/xdata-validate"); + return validate?.getChild("range"); + } + + inline public function get_regex() { + final validate = this.getChild("validate", "http://jabber.org/protocol/xdata-validate"); + return validate?.getChildText("regex"); + } + + inline public function get_required() { + return this.getChild("required") != null; + } + + @:to + inline public function toFormField(): FormField { + return this == null ? null : new FormField(this); + } +} + +@:expose +#if cpp +@:build(HaxeCBridge.expose()) +@:build(HaxeSwiftBridge.expose()) +#end +class FormField { + public final name: String; + public final label: Null<String>; + public final value: Array<String>; + public final required: Bool; + public final type: String; + public final datatype: String; + public final open: Bool; + public final rangeMin: Null<String>; + public final rangeMax: Null<String>; + public final regex: Null<String>; + + @:allow(borogove) + private function new(field: Field) { + name = field.name; + label = field.label; + value = field.value; + required = field.required; + type = field.type; + datatype = field.datatype; + open = field.open; + rangeMin = field.rangeMin; + rangeMax = field.rangeMax; + regex = field.regex; } } diff --git a/borogove/Form.hx b/borogove/Form.hx new file mode 100644 index 0000000..5b51684 --- /dev/null +++ b/borogove/Form.hx @@ -0,0 +1,208 @@ +package borogove; + +import borogove.DataForm; + +#if cpp +import HaxeCBridge; +#end + +@:expose +#if cpp +@:build(HaxeSwiftBridge.expose()) +#end +interface FormSection { + public function title(): Null<String>; + public function items(): Array<FormItem>; +} + +@:expose +#if cpp +@:build(HaxeCBridge.expose()) +@:build(HaxeSwiftBridge.expose()) +#end +class FormItem { + public final text: Null<String>; + public final field: Null<FormField>; + public final section: Null<FormSection>; + + @:allow(borogove) + private function new(text: Null<String>, field: Null<FormField>, section: Null<FormSection>) { + this.text = text; + this.field = field; + this.section = section; + } +} + +#if cpp +@:build(HaxeCBridge.expose()) +@:build(HaxeSwiftBridge.expose()) +#end +class FormSubmitBuilder { + private final data: Map<String, Array<String>> = []; + + public function new() { } + + public function add(k: String, v: String) { + if (data.has(k)) { + data.set(k, data.get(k).concat([v])); + } else { + data.set(k, [v]); + } + } + + @:allow(borogove) + private function submit(form: DataForm) { + final toSubmit = new Stanza("x", { xmlns: "jabber:x:data", type: "submit" }); + for (f in form.fields) { + if (!data.has(f.name) && f.value.length > 0) { + final tag = toSubmit.tag("field", { "var": f.name }); + for (v in f.value) { + tag.textTag("value", v); + } + tag.up(); + } else if (f.required && (!data.has(f.name) || data[f.name].length < 1)) { + trace("No value provided for required field", f.name); + return null; + } + } + for (k => vs in data) { + final tag = toSubmit.tag("field", { "var": k }); + for (v in vs) { + tag.textTag("value", v); + } + tag.up(); + } + + return toSubmit; + } +} + +typedef StringOrArray = haxe.extern.EitherType<String, Array<String>>; + +@:expose +#if cpp +@:build(HaxeCBridge.expose()) +@:build(HaxeSwiftBridge.expose()) +#end +class Form implements FormSection { + private final form: DataForm; + + @:allow(borogove) + private function new(form: DataForm) { + this.form = form; + } + + public function title() { + return form.title; + } + + public function items() { + final s: Stanza = form; + final hasLayout = s.getChild("page", "http://jabber.org/protocol/xdata-layout") != null; + final items = []; + for (child in s.allTags()) { + if (child.name == "instructions" && (child.attr.get("xmlns") == null || child.attr.get("xmlns") == "jabber:x:data")) { + items.push(new FormItem(child.getText(), null, null)); + } + if (!hasLayout && child.name == "field" && (child.attr.get("xmlns") == null || child.attr.get("xmlns") == "jabber:x:data")) { + final fld: Null<Field> = child; + if (fld.type != "hidden") { + items.push(new FormItem(null, fld, null)); + } + } + if (!hasLayout && child.name == "reported" && (child.attr.get("xmlns") == null || child.attr.get("xmlns") == "jabber:x:data")) { + throw "TODO"; + } + if (child.name == "page" && child.attr.get("xmlns") == "http://jabber.org/protocol/xdata-layout") { + items.push(new FormItem(null, null, new FormLayoutSection(form, child))); + } + } + + return items; + } + + #if js + @:allow(borogove) + private function submit( + data: haxe.extern.EitherType< + haxe.extern.EitherType< + haxe.DynamicAccess<StringOrArray>, + Map<String, StringOrArray> + >, + js.html.FormData + > + ) { + final builder = new FormSubmitBuilder(); + + if (Std.isOfType(data, js.lib.Map)) { + for (k => v in ((cast data) : Map<String, StringOrArray>)) { + if (Std.isOfType(v, String)) { + builder.add(k, v); + } else { + for (oneV in ((cast v) : Array<String>)) { + builder.add(k, oneV); + } + } + } + #if !nodejs + } else if (Std.isOfType(data, js.html.FormData)) { + for (entry in new js.lib.HaxeIterator(((cast data) : js.html.FormData).entries())) { + builder.add(entry[0], entry[1]); + } + #end + } else { + for (k => v in ((cast data) : haxe.DynamicAccess<StringOrArray>)) { + if (Std.isOfType(v, String)) { + builder.add(k, v); + } else { + for (oneV in ((cast v) : Array<String>)) { + builder.add(k, oneV); + } + } + } + } + + return builder.submit(form); + } + #else + @:allow(borogove) + private function submit(data: FormSubmitBuilder) { + return data.submit(form); + } + #end +} + +class FormLayoutSection implements FormSection { + private final form: DataForm; + private final section: Stanza; + + @:allow(borogove) + private function new(form: DataForm, section: Stanza) { + this.form = form; + this.section = section; + } + + public function title() { + return section.attr.get("label"); + } + + public function items() { + final items = []; + for (child in section.allTags()) { + if (child.name == "text" && (child.attr.get("xmlns") == null || child.attr.get("xmlns") == "http://jabber.org/protocol/xdata-layout")) { + items.push(new FormItem(child.getText(), null, null)); + } + if (child.name == "fieldref" && (child.attr.get("xmlns") == null || child.attr.get("xmlns") == "http://jabber.org/protocol/xdata-layout")) { + items.push(new FormItem(null, form.field(child.attr.get("var")), null)); + } + if (child.name == "reportedref" && (child.attr.get("xmlns") == null || child.attr.get("xmlns") == "http://jabber.org/protocol/xdata-layout")) { + throw "TODO"; + } + if (child.name == "section" && (child.attr.get("xmlns") == null || child.attr.get("xmlns") == "http://jabber.org/protocol/xdata-layout")) { + items.push(new FormItem(null, null, new FormLayoutSection(form, child))); + } + } + + return items; + } +} diff --git a/borogove/GenericStream.hx b/borogove/GenericStream.hx index f1bdd0a..4404049 100644 --- a/borogove/GenericStream.hx +++ b/borogove/GenericStream.hx @@ -1,6 +1,7 @@ package borogove; import haxe.io.BytesData; +import thenshim.Promise; import borogove.Stanza; import borogove.EventEmitter; @@ -18,9 +19,10 @@ abstract class GenericStream extends EventEmitter { public function new() { super(); } - + /* Connections and streams */ + abstract public function register(domain: String, preAuth: Null<String>):Promise<Stanza>; abstract public function connect(jid:String, sm:Null<BytesData>):Void; abstract public function disconnect():Void; abstract public function sendStanza(stanza:Stanza):Void; diff --git a/borogove/Register.hx b/borogove/Register.hx new file mode 100644 index 0000000..c635d4d --- /dev/null +++ b/borogove/Register.hx @@ -0,0 +1,149 @@ +package borogove; + +import thenshim.Promise; + +import borogove.Form; +import borogove.Stanza; +import borogove.Stream; +import borogove.Util; + +using StringTools; + +#if cpp +import HaxeCBridge; +#end + +@:expose +#if cpp +@:build(HaxeCBridge.expose()) +@:build(HaxeSwiftBridge.expose()) +#end +class Register { + private final stream: GenericStream; + private final username: Null<String>; + private final domain: String; + private final preAuth: Null<String>; + private var form: Null<Form> = null; + + private function new(domain: String, preAuth: Null<String>, username: Null<String>) { + stream = new Stream(); + this.domain = domain; + this.preAuth = preAuth; + this.username = username; + } + + /** + Start new registration flow for a given domain or invite URL + **/ + public static function fromDomainOrInvite(domainOrInvite: String) { + if (domainOrInvite.startsWith("xmpp:")) { + return Promise.resolve(fromXmppURI(domainOrInvite)); + } else if (domainOrInvite.startsWith("https://")) { + return xmppLinkHeader(domainOrInvite).then(xmppUri -> { + return fromXmppURI(xmppUri); + }); + } else { + return Promise.resolve(new Register(domainOrInvite, null, null)); + } + } + + private static function fromXmppURI(xmppUri: String) { + final parts = xmppUri.substr(5).split("?"); + final authParts = parts[0].split("@"); + final domain = uriDecode(authParts.length > 1 ? authParts[1] : authParts[0]); + var preAuth: Null<String> = null; + var username: Null<String> = null; + if (parts.length > 1) { + final queryParts = parts[1].split(";"); + for (part in queryParts) { + if (part == "register" && authParts.length > 1) username = uriDecode(authParts[0]); + if (part.startsWith("preauth=")) { + preAuth = uriDecode(part.substr(8)); + } + } + } + return new Register(domain, preAuth, username); + } + + /** + Fetch registration form from the server. + If you already know what fields your server wants, this is optional. + **/ + public function getForm() { + return stream.register(domain, preAuth).then(reply -> { + final error = getError(reply); + if (error != null) return Promise.reject(error); + + final query = reply.getChild("query", "jabber:iq:register"); + final form: DataForm = query.getChild("x", "jabber:x:data"); + if (form == null) { + return Promise.reject("No form found"); + } + + if (username != null) { + final fuser = form.field("username"); + fuser.value = [username]; + (fuser : Stanza).attr.set("type", "fixed"); + } + + this.form = new Form(form); + return Promise.resolve(this.form); + }); + } + + /** + Submit registration data to the server + **/ + #if js + public function submit( + data: haxe.extern.EitherType< + haxe.extern.EitherType< + haxe.DynamicAccess<StringOrArray>, + Map<String, StringOrArray> + >, + js.html.FormData + > + ) + #else + public function submit(data: FormSubmitBuilder) + #end + : Promise<String> { + return (form == null ? getForm() : Promise.resolve(null)).then(_ -> { + final toSubmit: DataForm = form.submit(data); + if (toSubmit == null) return Promise.reject("Invalid submission"); + + return new Promise((resolve, reject) -> { + stream.sendIq( + new Stanza("iq", { type: "set", to: domain }) + .tag("query", { xmlns: "jabber:iq:register" }) + .addChild(toSubmit), + (reply) -> { + final error = getError(reply); + if (error != null) return reject(error); + + // It is conventional for username@domain to be the registered JID + // IBR doesn't really give us a better option right now + resolve(toSubmit.field("username")?.value?.join("") + "@" + domain); + } + ); + }); + }); + } + + /** + Disconnect from the server after registration is done + **/ + public function disconnect() { + stream.disconnect(); + } + + private function getError(iq: Stanza) { + if (iq.attr.get("type") == "error") { + final error = iq.getChild("error"); + final text = error.getChildText("text", "urn:ietf:params:xml:ns:xmpp-stanzas"); + return text ?? error.getFirstChild()?.name ?? "error"; + } + + return null; + } +} diff --git a/borogove/Util.hx b/borogove/Util.hx index f6bdf5a..f33949d 100644 --- a/borogove/Util.hx +++ b/borogove/Util.hx @@ -1,6 +1,7 @@ package borogove; import haxe.io.Bytes; +import thenshim.Promise; #if js import js.html.TextEncoder; final textEncoder = new TextEncoder(); @@ -22,6 +23,26 @@ function setupTrace() { #end } +function xmppLinkHeader(url: String) { + return new Promise((resolve, reject) -> { + tink.http.Client.fetch(url, { method: HEAD }) + .all() + .handle(function(o) switch o { + case Success(res): + final regex = ~/<(xmpp:[^>]+)>/; + for (link in res.header.get("link")) { + if (regex.match(link)) { + resolve(regex.matched(1)); + return; + } + } + reject(null); + case Failure(e): + reject(e); + }); + }); +} + inline function bytesOfString(s: String) { #if js return Bytes.ofData(textEncoder.encode(s).buffer); @@ -64,6 +85,43 @@ function xmlEscape(s: String) { return StringTools.replace(StringTools.replace(StringTools.replace(s, "&", "&"), "<", "<"), ">", ">"); } +// Taken from StringTools but modified not to decode + as space +#if (flash || js || cs || python) inline #end function uriDecode(s:String):String { + #if flash + return untyped __global__["decodeURIComponent"](s); + #elseif js + return untyped decodeURIComponent(s); + #elseif cs + return untyped cs.system.Uri.UnescapeDataString(s); + #elseif python + return python.lib.urllib.Parse.unquote(s); + #elseif lua + s = lua.NativeStringTools.gsub(s, "%%(%x%x)", function(h) { + return lua.NativeStringTools.char(lua.Lua.tonumber(h, 16)); + }); + return s; + #else + final bytes = new haxe.io.BytesBuffer(); + var i = 0; + + while (i < s.length) { + var c = s.charAt(i); + if (c == "%" && i + 2 < s.length) { + final value = Std.parseInt("0x" + s.substr(i + 1, 2)); + if (value != null) { + bytes.addByte(value); + i += 3; + continue; + } + } + bytes.addString(c, UTF8); + i++; + } + + return bytes.getBytes().toString(); // UTF-8 decode + #end +} + macro function getGitVersion():haxe.macro.Expr.ExprOf<String> { #if !display var process = new sys.io.Process('git', ['describe', '--always']); diff --git a/borogove/streams/XmppJsStream.hx b/borogove/streams/XmppJsStream.hx index 746f693..1c7b852 100644 --- a/borogove/streams/XmppJsStream.hx +++ b/borogove/streams/XmppJsStream.hx @@ -24,6 +24,7 @@ extern class XmppJsClient { function stop():Promise<Dynamic>; function on(eventName:String, callback:(Dynamic)->Void):Void; function send(stanza:XmppJsXml):Void; + final NS:String; var jid:XmppJsJID; var streamFrom:Null<XmppJsJID>; var status: String; @@ -100,6 +101,47 @@ extern class XmppJsError { public final application: String; } + +@:js.import("@xmpp/client-core", "Client") +extern class XmppJsClientCore { + function new(params: { service: String, domain: String }); + function start():Promise<Dynamic>; +} + +@:js.import(@default "@xmpp/websocket") +extern class XmppJsWebsocket { + function new(params: { entity: XmppJsClientCore }); +} + +#if nodejs +@:js.import(@default "@xmpp/tcp") +extern class XmppJsTcp { + function new(params: { entity: XmppJsClientCore }); +} + + +@:js.import(@default "@xmpp/starttls") +extern class XmppJsSTARTTLS { + function new(params: { streamFeatures: XmppJsStreamFeatures }); +} +#end + +@:js.import(@default "@xmpp/resolve") +extern class XmppJsResolve { + function new(params: { entity: XmppJsClientCore }); +} + +@:js.import(@default "@xmpp/middleware") +extern class XmppJsMiddleware { + function new(params: { entity: XmppJsClientCore }); +} + +@:js.import(@default "@xmpp/stream-features") +extern class XmppJsStreamFeatures { + function new(params: { middleware: XmppJsMiddleware }); + function use(feature: String, ns: String, cb: (ctx: Any, next: ()->Void, feature: Any) -> Promise<Void>):Void; +} + class XmppJsStream extends GenericStream { private var client:XmppJsClient; private var jid:XmppJsJID; @@ -130,6 +172,63 @@ class XmppJsStream extends GenericStream { }, "offline"); } + public function register(domain: String, preAuth: Null<String>) { + final entity = new XmppJsClientCore({ service: domain, domain: domain }); + final middleware = new XmppJsMiddleware({ entity: entity }); + final streamFeatures = new XmppJsStreamFeatures({ middleware: middleware }); + + return new Promise((resolve, reject) -> { + if (preAuth != null) { + streamFeatures.use("register", "urn:xmpp:ibr-token:0", (ctx, next, feature) -> { + client.status = "online"; + this.sendIq( + new Stanza("iq", { type: "set", to: domain }).tag("preauth", { xmlns: "urn:xmpp:pars:0", token: preAuth }), + reply -> { + if (reply.attr.get("type") == "error") { + resolve(reply); + } else { + next(); + } + } + ); + return Promise.resolve(null); + }); + } + + streamFeatures.use("register", "http://jabber.org/features/iq-register", (ctx, next, feature) -> { + client.status = "online"; + this.sendIq( + new Stanza("iq", { type: "get", to: domain }).tag("query", { xmlns: "jabber:iq:register" }), + resolve + ); + return Promise.resolve(null); + }); + + client = cast js.lib.Object.assign( + entity, + #if nodejs + new XmppJsTcp({ entity: entity }), + #end + new XmppJsWebsocket({ entity: entity }), + middleware, + streamFeatures, + new XmppJsResolve({ entity: entity }), + #if nodejs + new XmppJsSTARTTLS({ streamFeatures: streamFeatures }), + #end + ); + + client.on("stanza", function (stanza) { + this.onStanza(convertToStanza(stanza, client.NS)); + }); + + client.start().catchError((err) -> { + trace(err); + reject(err); + }); + }); + } + public function connect(jidS:String, sm:Null<BytesData>) { this.state.event("connect-requested"); this.jid = new XmppJsJID(jidS); @@ -226,7 +325,7 @@ class XmppJsStream extends GenericStream { xmpp.on("stanza", function (stanza) { triggerSMupdate(); - this.onStanza(convertToStanza(stanza)); + this.onStanza(convertToStanza(stanza, client.NS)); }); xmpp.streamManagement.on("ack", (stanza) -> { @@ -283,8 +382,10 @@ class XmppJsStream extends GenericStream { return xml; } - private static function convertToStanza(el:XmppJsXml):Stanza { - var stanza = new Stanza(el.name, el.attrs); + private static function convertToStanza(el:XmppJsXml, xmlns:Null<String> = null):Stanza { + var attrs: haxe.DynamicAccess<String> = el.attrs ?? {}; + if (attrs.get("xmlns") == null && xmlns != null) attrs.set("xmlns", xmlns); + var stanza = new Stanza(el.name, attrs); for (child in el.children) { if(XmppJsLtx.isText(child)) { stanza.text(cast(child, String)); @@ -313,7 +414,7 @@ class XmppJsStream extends GenericStream { } private function triggerSMupdate() { - if (client == null || !client.streamManagement.enabled || !emitSMupdates) return; + if (client == null || !client.streamManagement?.enabled || !emitSMupdates) return; this.trigger( "sm/update", { diff --git a/borogove/streams/XmppStropheStream.hx b/borogove/streams/XmppStropheStream.hx index f87d04c..6d6e86d 100644 --- a/borogove/streams/XmppStropheStream.hx +++ b/borogove/streams/XmppStropheStream.hx @@ -3,6 +3,7 @@ package borogove.streams; import haxe.DynamicAccess; import haxe.io.Bytes; import haxe.io.BytesData; +import thenshim.Promise; import cpp.Char; import cpp.ConstPointer; @@ -318,6 +319,10 @@ class XmppStropheStream extends GenericStream { } } + public function register(domain: String, preAuth: Null<String>) { + return Promise.reject("TODO"); + } + public function connect(jid:String, sm:Null<BytesData>) { StropheConn.set_jid(conn, NativeString.c_str(jid)); this.on("auth/password", function (event) { diff --git a/browserjs.hxml b/browserjs.hxml index 48d3979..6646135 100644 --- a/browserjs.hxml +++ b/browserjs.hxml @@ -9,6 +9,7 @@ --library tink_http borogove.Client +borogove.Register borogove.Push borogove.Version borogove.persistence.Sqlite diff --git a/nodejs.hxml b/nodejs.hxml index 8bc330c..9d36c8f 100644 --- a/nodejs.hxml +++ b/nodejs.hxml @@ -10,6 +10,7 @@ --library tink_http borogove.Client +borogove.Register borogove.Push borogove.Version borogove.persistence.Sqlite diff --git a/npm/index.ts b/npm/index.ts index 101a04e..57558b1 100644 --- a/npm/index.ts +++ b/npm/index.ts @@ -15,6 +15,9 @@ export import Config = borogove.Config; export import CustomEmojiReaction = borogove.CustomEmojiReaction; export import DirectChat = borogove.DirectChat; export import EventEmitter = borogove.EventEmitter; +export import FormSection = borogove.FormSection; +export import FormItem = borogove.FormItem; +export import FormField = borogove.FormField; export import Hash = borogove.Hash; export import Identicon = borogove.Identicon; export import Notification = borogove.Notification; @@ -22,6 +25,7 @@ export import Participant = borogove.Participant; export import Persistence = borogove.Persistence; export import Push = borogove.Push; export import Reaction = borogove.Reaction; +export import Register = borogove.Register; export import SerializedChat = borogove.SerializedChat; export const VERSION = borogove.Version.HUMAN; export import calls = borogove.calls;