From 33f9dc5337f1a59932b7ac117b0dea56e1dafe1a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Remko=20Tron=C3=A7on?= <git@el-tramo.be>
Date: Fri, 20 Sep 2013 22:32:57 +0200
Subject: Sluift: Add support for ad-hoc commands

Change-Id: I4ac2d0b07841b03086d9dbd9fa06d1f030f4e1ca

diff --git a/Sluift/ElementConvertors/CommandConvertor.cpp b/Sluift/ElementConvertors/CommandConvertor.cpp
new file mode 100644
index 0000000..7fb7b22
--- /dev/null
+++ b/Sluift/ElementConvertors/CommandConvertor.cpp
@@ -0,0 +1,195 @@
+/*
+ * Copyright (c) 2013 Remko Tronçon
+ * Licensed under the GNU General Public License.
+ * See the COPYING file for more information.
+ */
+
+#include <Sluift/ElementConvertors/CommandConvertor.h>
+
+#include <lua.hpp>
+#include <boost/smart_ptr/make_shared.hpp>
+#include <boost/numeric/conversion/cast.hpp>
+
+#include <Swiften/Base/foreach.h>
+#include <Sluift/Lua/Check.h>
+#include <Sluift/Lua/Value.h>
+#include <Sluift/LuaElementConvertors.h>
+
+using namespace Swift;
+
+static Command::Action convertActionFromString(const std::string& action) {
+	if (action == "cancel") { return Command::Cancel; }
+	else if (action == "execute") { return Command::Execute; }
+	else if (action == "complete") { return Command::Complete; }
+	else if (action == "prev") { return Command::Prev; }
+	else if (action == "next") { return Command::Next; }
+	return Command::NoAction;
+}
+
+static std::string convertActionToString(Command::Action action) {
+	switch (action) {
+		case Command::Cancel: return "cancel";
+		case Command::Execute: return "execute";
+		case Command::Complete: return "complete";
+		case Command::Prev: return "prev";
+		case Command::Next: return "next";
+		case Command::NoAction: assert(false); return "";
+	}
+	assert(false);
+	return "";
+}
+
+CommandConvertor::CommandConvertor(LuaElementConvertors* convertors) : 
+		GenericLuaElementConvertor<Command>("command"),
+		convertors(convertors) {
+}
+
+CommandConvertor::~CommandConvertor() {
+}
+
+boost::shared_ptr<Command> CommandConvertor::doConvertFromLua(lua_State* L) {
+	boost::shared_ptr<Command> result = boost::make_shared<Command>();
+
+	lua_getfield(L, -1, "node");
+	if (!lua_isnil(L, -1)) {
+		result->setNode(std::string(Lua::checkString(L, -1)));
+	}
+	lua_pop(L, 1);
+
+	lua_getfield(L, -1, "session_id");
+	if (!lua_isnil(L, -1)) {
+		result->setSessionID(std::string(Lua::checkString(L, -1)));
+	}
+	lua_pop(L, 1);
+
+	lua_getfield(L, -1, "status");
+	if (!lua_isnil(L, -1)) {
+		std::string statusText = Lua::checkString(L, -1);
+		Command::Status status = Command::NoStatus;
+		if (statusText == "executing") { status = Command::Executing; }
+		else if (statusText == "completed") { status = Command::Completed; }
+		else if (statusText == "canceled") { status = Command::Canceled; }
+		result->setStatus(status);
+	}
+	lua_pop(L, 1);
+
+	lua_getfield(L, -1, "action");
+	if (!lua_isnil(L, -1)) {
+		result->setAction(convertActionFromString(Lua::checkString(L, -1)));
+	}
+	lua_pop(L, 1);
+
+	lua_getfield(L, -1, "execute_action");
+	if (!lua_isnil(L, -1)) {
+		result->setExecuteAction(convertActionFromString(Lua::checkString(L, -1)));
+	}
+	lua_pop(L, 1);
+
+	lua_getfield(L, -1, "available_actions");
+	if (!lua_isnil(L, -1)) {
+		Lua::checkType(L, -1, LUA_TTABLE);
+		lua_pushnil(L);
+		for (lua_pushnil(L); lua_next(L, -2) != 0; ) {
+			result->addAvailableAction(convertActionFromString(Lua::checkString(L, -1)));
+			lua_pop(L, 1);
+		}
+	}
+	lua_pop(L, 1);
+
+	lua_getfield(L, -1, "notes");
+	if (!lua_isnil(L, -1)) {
+		Lua::checkType(L, -1, LUA_TTABLE);
+		lua_pushnil(L);
+		for (lua_pushnil(L); lua_next(L, -2) != 0; ) {
+			Lua::checkType(L, -1, LUA_TTABLE);
+			std::string note;
+			lua_getfield(L, -1, "note");
+			if (!lua_isnil(L, -1)) {
+				note = Lua::checkString(L, -1);
+			}
+			lua_pop(L, 1);
+
+			Command::Note::Type noteType = Command::Note::Info;
+			lua_getfield(L, -1, "type");
+			if (!lua_isnil(L, -1)) {
+				std::string type = Lua::checkString(L, -1);
+				if (type == "info") { noteType = Command::Note::Info; }
+				else if (type == "warn") { noteType = Command::Note::Warn; }
+				else if (type == "error") { noteType = Command::Note::Error; }
+			}
+			lua_pop(L, 1);
+
+			result->addNote(Command::Note(note, noteType));
+			lua_pop(L, 1);
+		}
+	}
+	lua_pop(L, 1);
+
+	lua_getfield(L, -1, "form");
+	if (!lua_isnil(L, -1)) {
+		if (boost::shared_ptr<Form> form = boost::dynamic_pointer_cast<Form>(convertors->convertFromLuaUntyped(L, -1, "form"))) {
+			result->setForm(form);
+		}
+	}
+	lua_pop(L, 1);
+
+	return result;
+}
+
+void CommandConvertor::doConvertToLua(lua_State* L, boost::shared_ptr<Command> payload) {
+	Lua::Table result;
+	if (!payload->getNode().empty()) {
+		result["node"] = Lua::valueRef(payload->getNode());
+	}
+	if (!payload->getSessionID().empty()) {
+		result["session_id"] = Lua::valueRef(payload->getSessionID());
+	}
+	switch (payload->getStatus()) {
+		case Command::Executing: result["status"] = Lua::valueRef("executing"); break;
+		case Command::Completed: result["status"] = Lua::valueRef("completed"); break;
+		case Command::Canceled: result["status"] = Lua::valueRef("canceled"); break;
+		case Command::NoStatus: break;
+	}
+
+	if (!payload->getNotes().empty()) {
+		std::vector<Lua::Value> notes;
+		foreach (const Command::Note& note, payload->getNotes()) {
+			Lua::Table noteTable;
+			if (!note.note.empty()) {
+				noteTable["note"] = Lua::valueRef(note.note);
+			}
+			switch (note.type) {
+				case Command::Note::Info: noteTable["type"] = Lua::valueRef("info"); break;
+				case Command::Note::Warn: noteTable["type"] = Lua::valueRef("warn"); break;
+				case Command::Note::Error: noteTable["type"] = Lua::valueRef("error"); break;
+			}
+			notes.push_back(noteTable);
+		}
+		result["notes"] = Lua::valueRef(notes);
+	}
+
+	if (payload->getAction() != Command::NoAction) {
+		result["action"] = Lua::valueRef(convertActionToString(payload->getAction()));
+	}
+
+	if (payload->getExecuteAction() != Command::NoAction) {
+		result["execute_action"] = Lua::valueRef(convertActionToString(payload->getAction()));
+	}
+
+	if (!payload->getAvailableActions().empty()) {
+		std::vector<Lua::Value> availableActions;
+		foreach (const Command::Action& action, payload->getAvailableActions()) {
+			if (action != Command::NoAction) {
+				availableActions.push_back(convertActionToString(action));
+			}
+		}
+		result["available_actions"] = Lua::valueRef(availableActions);
+	}
+
+	Lua::pushValue(L, result);
+
+	if (payload->getForm()) {
+		convertors->convertToLuaUntyped(L, payload->getForm());
+		lua_setfield(L, -2, "form");
+	}
+}
diff --git a/Sluift/ElementConvertors/CommandConvertor.h b/Sluift/ElementConvertors/CommandConvertor.h
new file mode 100644
index 0000000..7008b50
--- /dev/null
+++ b/Sluift/ElementConvertors/CommandConvertor.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2013 Remko Tronçon
+ * Licensed under the GNU General Public License.
+ * See the COPYING file for more information.
+ */
+
+#pragma once
+
+#include <Swiften/Base/Override.h>
+
+#include <Sluift/GenericLuaElementConvertor.h>
+#include <Swiften/Elements/Command.h>
+
+namespace Swift {
+	class LuaElementConvertors;
+
+	class CommandConvertor : public GenericLuaElementConvertor<Command> {
+		public:
+			CommandConvertor(LuaElementConvertors* convertors);
+			virtual ~CommandConvertor();
+
+			virtual boost::shared_ptr<Command> doConvertFromLua(lua_State*) SWIFTEN_OVERRIDE;
+			virtual void doConvertToLua(lua_State*, boost::shared_ptr<Command>) SWIFTEN_OVERRIDE;
+
+		private:
+			LuaElementConvertors* convertors;
+	};
+}
diff --git a/Sluift/Examples/AdHocCommands.lua b/Sluift/Examples/AdHocCommands.lua
new file mode 100644
index 0000000..9e83f0c
--- /dev/null
+++ b/Sluift/Examples/AdHocCommands.lua
@@ -0,0 +1,48 @@
+--[[
+	Copyright (c) 2013 Remko Tronçon
+	Licensed under the GNU General Public License v3.
+	See Documentation/Licenses/GPLv3.txt for more information.
+--]]
+
+--[[
+	
+	Ad-hoc command example.
+
+	This script logs into an XMPP server, and executes a
+	multi-step ad-hoc command.
+	
+	The following environment variables are used:
+	* SLUIFT_JID, SWIFT_PASS: JID and password to log in with
+	* SLUIFT_DEBUG: Sets whether debugging should be turned on
+
+--]]
+
+require "sluift"
+
+sluift.debug = os.getenv("SLUIFT_DEBUG") or false
+
+c = sluift.new_client(os.getenv("SLUIFT_JID"), os.getenv("SLUIFT_PASS"))
+c:connect(function () 
+	to = sluift.jid.domain(os.getenv("SLUIFT_JID"))
+
+	-- Get the list of commands
+	commands = assert(c:get_disco_items{ to = to, disco_items = {
+		node = sluift.disco.features.COMMANDS }})
+	print(commands)
+
+	-- Get the initial form
+	result = assert(c:set_command{ to = to, command = {
+			action = 'execute', node = 'http://jabber.org/protocol/admin#get-user-roster'}})
+
+	-- Fill in the form
+	submission = result.form:create_submission()
+	submission.accountjid = os.getenv("SLUIFT_JID")
+
+	-- Submit the form
+	result = assert(c:set_command{ to = to, command = {
+			action = 'complete', node = 'http://jabber.org/protocol/admin#get-user-roster',
+			session_id = result.session_id, form = submission}})
+	for _, v in ipairs(result.form.roster.values) do
+		print(v)
+	end
+end)
diff --git a/Sluift/LuaElementConvertors.cpp b/Sluift/LuaElementConvertors.cpp
index cadfbc4..71957c1 100644
--- a/Sluift/LuaElementConvertors.cpp
+++ b/Sluift/LuaElementConvertors.cpp
@@ -21,6 +21,7 @@
 #include <Sluift/ElementConvertors/VCardUpdateConvertor.h>
 #include <Sluift/ElementConvertors/VCardConvertor.h>
 #include <Sluift/ElementConvertors/BodyConvertor.h>
+#include <Sluift/ElementConvertors/CommandConvertor.h>
 #include <Sluift/Lua/LuaUtils.h>
 #include <Sluift/Lua/Exception.h>
 
@@ -28,6 +29,7 @@ using namespace Swift;
 
 LuaElementConvertors::LuaElementConvertors() {
 	registerConvertors();
+	convertors.push_back(boost::make_shared<CommandConvertor>(this));
 	convertors.push_back(boost::make_shared<PubSubEventConvertor>(this));
 	convertors.push_back(boost::make_shared<BodyConvertor>());
 	convertors.push_back(boost::make_shared<VCardConvertor>());
diff --git a/Sluift/SConscript b/Sluift/SConscript
index 5d2242e..1a29e43 100644
--- a/Sluift/SConscript
+++ b/Sluift/SConscript
@@ -29,6 +29,7 @@ elif env["SCONS_STAGE"] == "build" :
 		"ElementConvertors/FormConvertor.cpp",
 		"ElementConvertors/SoftwareVersionConvertor.cpp",
 		"ElementConvertors/VCardConvertor.cpp",
+		"ElementConvertors/CommandConvertor.cpp",
 		"ClientHelpers.cpp",
 		"SluiftClient.cpp",
 		"boot.c",
diff --git a/Sluift/boot.lua b/Sluift/boot.lua
index e81257a..2505736 100644
--- a/Sluift/boot.lua
+++ b/Sluift/boot.lua
@@ -173,13 +173,25 @@ for method, event_type in pairs({messages = 'message', pubsub_events = 'pubsub'}
 	end
 end
 
--- Register get_* convenience methods for some type of queries
-for _, query_type in ipairs({'software_version', 'disco_items', 'xml', 'dom', 'vcard'}) do
-	Client['get_' .. query_type] = function (client, options)
-		options = options or {}
-		if type(options) ~= 'table' then error('Invalid options: ' .. options) end 
-		options['query'] = merge_tables({_type = query_type}, options['query'] or {})
-		return client:get(options)
+--
+-- Register get_* and set_* convenience methods for some type of queries
+--
+-- Example usages:
+--	client:get_software_version{to = 'alice@wonderland.lit'}
+--	client:set_command{to = 'alice@wonderland.lit', command = { type = 'execute', node = 'uptime' }}
+--
+local get_set_shortcuts = {
+	get = {'software_version', 'disco_items', 'xml', 'dom', 'vcard'},
+	set = {'command'}
+}
+for query_action, query_types in pairs(get_set_shortcuts) do
+	for _, query_type in ipairs(query_types) do
+		Client[query_action .. '_' .. query_type] = function (client, options)
+			options = options or {}
+			if type(options) ~= 'table' then error('Invalid options: ' .. options) end 
+			options['query'] = merge_tables({_type = query_type}, options[query_type] or {})
+			return client[query_action](client, options)
+		end
 	end
 end
 
@@ -403,6 +415,7 @@ end
 local disco = {
 	features = {
 		DISCO_INFO = 'http://jabber.org/protocol/disco#info',
+		COMMANDS = 'http://jabber.org/protocol/commands',
 		USER_LOCATION = 'http://jabber.org/protocol/geoloc',
 		USER_TUNE = 'http://jabber.org/protocol/tune',
 		USER_AVATAR_METADATA = 'urn:xmpp:avatar:metadata',
-- 
cgit v0.10.2-6-g49f6