| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2025-11-19 14:14:47 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2025-11-19 14:14:47 UTC |
| parent | 76d29aeeffd8589eb7bcbe3b01454481fd9a60ab |
| borogove/DataForm.hx | +63 | -1 |
| borogove/Form.hx | +52 | -17 |
| borogove/OOB.hx | +15 | -0 |
| borogove/Register.hx | +15 | -5 |
| npm/index.ts | +2 | -0 |
diff --git a/borogove/DataForm.hx b/borogove/DataForm.hx index f4af46a..90ae52d 100644 --- a/borogove/DataForm.hx +++ b/borogove/DataForm.hx @@ -9,12 +9,17 @@ import HaxeCBridge; @:forward(toString) abstract DataForm(Stanza) from Stanza to Stanza { public var title(get, never): Null<String>; + public var type(get, never): Null<String>; public var fields(get, never): Array<Field>; inline public function get_title() { return this.getChildText("title"); } + inline public function get_type() { + return this.attr.get("type") ?? "form"; + } + inline public function get_fields() { return this.allTags("field"); } @@ -31,9 +36,11 @@ abstract DataForm(Stanza) from Stanza to Stanza { abstract Field(Stanza) from Stanza to Stanza { public var name(get, never): String; public var label(get, never): Null<String>; + public var desc(get, never): Null<String>; public var value(get, set): Array<String>; - public var type(get, never): String; + public var type(get, set): String; public var datatype(get, never): String; + public var options(get, never): Array<Option>; public var open(get, never): Bool; public var rangeMin(get, never): Null<String>; public var rangeMax(get, never): Null<String>; @@ -48,6 +55,10 @@ abstract Field(Stanza) from Stanza to Stanza { return this.attr.get("label"); } + inline public function get_desc() { + return this.getChildText("desc"); + } + public function get_value() { final isbool = (this : Field).datatype == "xs:boolean"; return this.allTags("value").map(v -> { @@ -77,6 +88,10 @@ abstract Field(Stanza) from Stanza to Stanza { return attr; } + inline public function set_type(newType: String) { + return this.attr.set("type", newType); + } + public function get_datatype() { final validate = this.getChild("validate", "http://jabber.org/protocol/xdata-validate"); if (validate != null && validate.attr.get("datatype") != null) { @@ -89,6 +104,10 @@ abstract Field(Stanza) from Stanza to Stanza { return "xs:string"; } + public function get_options() { + return this.allTags("option"); + } + inline public function get_open() { final validate = this.getChild("validate", "http://jabber.org/protocol/xdata-validate"); return validate?.getChild("open") != null; @@ -122,6 +141,24 @@ abstract Field(Stanza) from Stanza to Stanza { } } +abstract Option(Stanza) from Stanza to Stanza { + public var label(get, never): Null<String>; + public var value(get, never): Null<String>; + + inline public function get_label() { + return this.attr.get("label"); + } + + inline public function get_value() { + return this.getChildText("value"); + } + + @:to + inline public function toFormOption(): FormOption { + return this == null ? null : FormOption.fromOption(this); + } +} + @:expose #if cpp @:build(HaxeCBridge.expose()) @@ -130,10 +167,12 @@ abstract Field(Stanza) from Stanza to Stanza { class FormField { public final name: String; public final label: Null<String>; + public final desc: Null<String>; public final value: Array<String>; public final required: Bool; public final type: String; public final datatype: String; + public final options: Array<FormOption>; public final open: Bool; public final rangeMin: Null<String>; public final rangeMax: Null<String>; @@ -143,13 +182,36 @@ class FormField { private function new(field: Field) { name = field.name; label = field.label; + desc = field.desc; value = field.value; required = field.required; type = field.type; datatype = field.datatype; + options = field.options.map(o -> o.toFormOption()); open = field.open; rangeMin = field.rangeMin; rangeMax = field.rangeMax; regex = field.regex; } } + +@:expose +#if cpp +@:build(HaxeCBridge.expose()) +@:build(HaxeSwiftBridge.expose()) +#end +class FormOption { + public final label: Null<String>; + public final value: Null<String>; + + @:allow(borogove) + private function new(label: Null<String>, value: Null<String>) { + this.label = label; + this.value = value; + } + + @:allow(borogove) + private static function fromOption(option: Option) { + return new FormOption(option.label, option.value); + } +} diff --git a/borogove/Form.hx b/borogove/Form.hx index 5b51684..03694c3 100644 --- a/borogove/Form.hx +++ b/borogove/Form.hx @@ -24,12 +24,14 @@ class FormItem { public final text: Null<String>; public final field: Null<FormField>; public final section: Null<FormSection>; + public final status: Null<String>; @:allow(borogove) - private function new(text: Null<String>, field: Null<FormField>, section: Null<FormSection>) { + private function new(text: Null<String>, field: Null<FormField>, section: Null<FormSection>, status: Null<String> = null) { this.text = text; this.field = field; this.section = section; + this.status = status; } } @@ -51,18 +53,20 @@ class FormSubmitBuilder { } @:allow(borogove) - private function submit(form: DataForm) { + private function submit(form: Null<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); + if (form != null) { + 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; } - 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) { @@ -85,28 +89,59 @@ typedef StringOrArray = haxe.extern.EitherType<String, Array<String>>; @:build(HaxeSwiftBridge.expose()) #end class Form implements FormSection { - private final form: DataForm; + private final form: Null<DataForm>; + private final oob: Null<OOB>; @:allow(borogove) - private function new(form: DataForm) { + private function new(form: Null<DataForm>, oob: Null<OOB>) { + if (form == null && oob == null) throw "Need a form or OOB"; this.form = form; + this.oob = oob; + } + + /** + Is this form entirely results / read-only? + **/ + public function isResult() { + if (form == null) return true; + + return form.type == "result"; } + /** + Title of this form + **/ public function title() { - return form.title; + return form != null ? form.title : oob.desc; } + /** + URL to use instead of this form + **/ + public function url() { + return oob?.url; + } + + /** + Items to render inside this form + **/ public function items() { + if (form == null) return []; + 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)); + items.push(new FormItem(child.getText(), null, null, child.attr.get("type"))); } 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") { + if (fld.type == "fixed" && fld.label == null) { + for (v in fld.value) { + items.push(new FormItem(v, null, null)); + } + } else if (fld.type != "hidden") { items.push(new FormItem(null, fld, null)); } } @@ -150,7 +185,7 @@ class Form implements FormSection { builder.add(entry[0], entry[1]); } #end - } else { + } else if (data != null) { for (k => v in ((cast data) : haxe.DynamicAccess<StringOrArray>)) { if (Std.isOfType(v, String)) { builder.add(k, v); diff --git a/borogove/OOB.hx b/borogove/OOB.hx new file mode 100644 index 0000000..dc91185 --- /dev/null +++ b/borogove/OOB.hx @@ -0,0 +1,15 @@ +package borogove; + +@:forward(toString) +abstract OOB(Stanza) from Stanza to Stanza { + public var desc(get, never): Null<String>; + public var url(get, never): Null<String>; + + inline public function get_desc() { + return this.getChildText("desc"); + } + + inline public function get_url() { + return this.getChildText("url"); + } +} diff --git a/borogove/Register.hx b/borogove/Register.hx index 68ce33c..1e2fe4d 100644 --- a/borogove/Register.hx +++ b/borogove/Register.hx @@ -66,17 +66,18 @@ class Register { } /** - Fetch registration form from the server. + Fetch registration form options from the server. If you already know what fields your server wants, this is optional. **/ - public function getForm() { + public function getForm(): Promise<Array<Form>> { return stream.register(domain, preAuth).then(reply -> { final error = reply.getErrorText(); 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) { + final oob: OOB = query.getChild("x", "jabber:x:oob"); + if (form == null && oob == null) { return Promise.reject("No form found"); } @@ -86,8 +87,17 @@ class Register { (fuser : Stanza).attr.set("type", "fixed"); } - this.form = new Form(form); - return Promise.resolve(this.form); + final results = []; + if (form != null) { + this.form = new Form(form, null); + results.push(this.form); + } + if (oob != null) { + final oobForm = new Form(null, oob); + results.push(oobForm); + if (this.form == null) this.form = oobForm; + } + return Promise.resolve(results); }); } diff --git a/npm/index.ts b/npm/index.ts index e485a61..69635cf 100644 --- a/npm/index.ts +++ b/npm/index.ts @@ -16,9 +16,11 @@ export import Config = borogove.Config; export import CustomEmojiReaction = borogove.CustomEmojiReaction; export import DirectChat = borogove.DirectChat; export import EventEmitter = borogove.EventEmitter; +export import Form = borogove.Form; export import FormSection = borogove.FormSection; export import FormItem = borogove.FormItem; export import FormField = borogove.FormField; +export import FormOption = borogove.FormOption; export import Hash = borogove.Hash; export import Identicon = borogove.Identicon; export import Notification = borogove.Notification;