| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2025-11-19 14:17:06 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2025-11-19 14:17:06 UTC |
| parent | d364e6755a00b95b230b1c73b66e4f33cb70d6f6 |
| borogove/Command.hx | +80 | -0 |
| borogove/queries/CommandExecute.hx | +111 | -0 |
| npm/index.ts | +1 | -0 |
diff --git a/borogove/Command.hx b/borogove/Command.hx index 77ab86e..b47c697 100644 --- a/borogove/Command.hx +++ b/borogove/Command.hx @@ -4,6 +4,7 @@ import thenshim.Promise; import borogove.DataForm; import borogove.Form; +import borogove.queries.CommandExecute; @:expose @:allow(borogove.CommandSession) @@ -20,4 +21,83 @@ class Command { name = params.name ?? params.node; this.client = client; } + + /** + Start a new session for this command. May have side effects! + **/ + public function execute(): Promise<CommandSession> { + return new CommandSession("executing", null, [], [], this).execute(); + } +} + +@:expose +class CommandSession { + public final name: String; + public final status: String; + public final actions: Array<FormOption>; + public final forms: Array<Form>; + private final sessionid: String; + private final command: Command; + + @:allow(borogove) + private function new(status: String, sessionid: String, actions: Array<FormOption>, forms: Array<Form>, command: Command) { + this.name = forms[0]?.title() != null ? forms[0].title() : command.name; + this.status = status; + this.sessionid = sessionid; + this.actions = actions; + this.forms = forms; + this.command = command; + } + + #if js + public function execute( + action: Null<String> = null, + data: Null<haxe.extern.EitherType< + haxe.extern.EitherType< + haxe.DynamicAccess<StringOrArray>, + Map<String, StringOrArray> + >, + js.html.FormData + >> = null, + formIdx: Null<Int> = null + ) + #else + public function execute( + action: Null<String> = null, + data: Null<FormSubmitBuilder> = null, + formIdx: Null<Int> = null + ) + #end + : Promise<CommandSession> { + final extendedAction = action != null && !["prev", "next", "complete", "execute", "cancel"].contains(action); + var toSubmit = null; + if (data != null || extendedAction) { + toSubmit = forms[formIdx ?? 0].submit(data); + if (toSubmit == null) return Promise.reject("Invalid submission"); + } + + if (extendedAction) { + if (toSubmit == null) toSubmit = new Stanza("x", { xmlns: "jabber:x:data", type: "submit" }); + final dataForm: DataForm = toSubmit; + final fld = dataForm.field("http://jabber.org/protocol/commands#actions"); + if (fld == null) { + toSubmit.tag("field", { "var": "http://jabber.org/protocol/commands#actions" }).textTag("value", action).up(); + } else { + fld.value = [action]; + } + action = null; + } + + return new Promise((resolve, reject) -> { + final exe = new CommandExecute(command.jid.asString(), command.node, action, sessionid, toSubmit); + exe.onFinished(() -> { + if (exe.getResult(command) == null) { + reject(exe.responseStanza); + } else { + resolve(exe.getResult(command)); + } + }); + command.client.sendQuery(exe); + }); + } } diff --git a/borogove/queries/CommandExecute.hx b/borogove/queries/CommandExecute.hx new file mode 100644 index 0000000..3bef266 --- /dev/null +++ b/borogove/queries/CommandExecute.hx @@ -0,0 +1,111 @@ +package borogove.queries; + +import haxe.DynamicAccess; +import haxe.Exception; + +import borogove.ID; +import borogove.ResultSet; +import borogove.Stanza; +import borogove.queries.GenericQuery; +import borogove.Caps; +import borogove.Command; +import borogove.DataForm; +using borogove.Util; +using Lambda; + +class CommandExecute extends GenericQuery { + public var xmlns(default, null) = "http://jabber.org/protocol/commands"; + public var queryId:String = null; + public var responseStanza(default, null):Stanza; + private var result: Null<CommandSession>; + private final node: String; + + public function new(to: String, node: String, ?action: Null<String>, ?sessionid: Null<String>, ?payload: Null<Stanza>) { + this.node = node; + var attr: DynamicAccess<String> = { xmlns: xmlns, node: node }; + attr["action"] = action ?? "execute"; + if (sessionid != null) attr["sessionid"] = sessionid; + /* Build basic query */ + queryId = ID.short(); + queryStanza = new Stanza( + "iq", + { to: to, type: "set", id: queryId } + ).tag("command", attr); + if (payload != null) queryStanza.addChild(payload); + queryStanza.up(); + } + + public function handleResponse(stanza:Stanza) { + responseStanza = stanza; + finish(); + } + + @:access(borogove.Form.form) + public function getResult(command: Command) { + if (responseStanza == null) { + return null; + } + if(result == null) { + final cmd = responseStanza.getChild("command", xmlns); + if (responseStanza.attr.get("type") == "error" || cmd == null) { + result = new CommandSession( + "error", + queryStanza.attr.get("sessionid"), + [], + forms([responseStanza]), + command + ); + return result; + } + + if ( + queryStanza.attr.get("sessionid") != null && + cmd.attr.get("sessionid") != queryStanza.attr.get("sessionid") + ) { + trace("sessionid mismatch", queryStanza, cmd); + return null; + } + final forms = forms(cmd.allTags()); + final execute = cmd.getChild("actions")?.attr?.get("execute"); + final extActionsField = forms[0]?.form?.field("http://jabber.org/protocol/commands#actions"); + if (extActionsField != null) extActionsField.type = "hidden"; + final extActions: Array<FormOption> = (extActionsField?.options ?? []).map(o -> o.toFormOption()); + final actions = (cmd.getChild("actions")?.allTags()?.map(s -> new FormOption(s.name.capitalize(), s.name))?.filter(o -> o.value != "execute") ?? []).concat(extActions); + if (cmd.attr.get("status") == "executing") { + if (actions.length < 1) actions.push(new FormOption("Go", "execute")); + if (actions.find(a -> a.value == "cancel") == null) actions.push(new FormOption("Cancel", "cancel")); + } + actions.sort((x,y) -> x.value == execute ? -1 : (y.value == execute ? 1 : 0)); + result = new CommandSession( + cmd.attr.get("status"), + cmd.attr.get("sessionid"), + actions, + forms, + command + ); + } + return result; + } + + private function forms(els: Array<Stanza>): Array<Form> { + final fs = []; + for (el in els) { + if (el.name == "x" && el.attr.get("xmlns") == "jabber:x:data") { + fs.push(new Form(el, null)); + } + if (el.name == "x" && el.attr.get("xmlns") == "jabber:x:oob") { + fs.push(new Form(null, el)); + } + if (el.name == "iq" && el.attr.get("type") == "error") { + final error = el.getError(); + final formish = new Stanza("x", { xmlns: "jabber:x:data", type: "result" }).textTag("instructions", error.text ?? error.condition, { type: "error" }); + fs.push(new Form(formish, null)); + } + if (el.name == "note") { + final formish = new Stanza("x", { xmlns: "jabber:x:data", type: "result" }).textTag("instructions", el.getText(), { type: el.attr.get("type") }); + fs.push(new Form(formish, null)); + } + } + return fs; + } +} diff --git a/npm/index.ts b/npm/index.ts index 69635cf..a6d7d5a 100644 --- a/npm/index.ts +++ b/npm/index.ts @@ -12,6 +12,7 @@ export import ChatMessage = borogove.ChatMessage; export import ChatMessageBuilder = borogove.ChatMessageBuilder; export import Client = borogove.Client; export import Command = borogove.Command; +export import CommandSession = borogove.CommandSession; export import Config = borogove.Config; export import CustomEmojiReaction = borogove.CustomEmojiReaction; export import DirectChat = borogove.DirectChat;