git » sdk » commit 7062431

Support executing command

author Stephen Paul Weber
2025-11-19 14:17:06 UTC
committer Stephen Paul Weber
2025-11-19 14:17:06 UTC
parent d364e6755a00b95b230b1c73b66e4f33cb70d6f6

Support executing command

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;