From 522a3a1c66233792741eb47f75e009a2fe9fa3ad Mon Sep 17 00:00:00 2001
From: Kevin Smith <git@kismith.co.uk>
Date: Thu, 18 Nov 2010 17:01:22 +0000
Subject: Initial Ad-Hoc commands support.

This is not finished, use at your own peril.

diff --git a/Swift/Controllers/AdHocManager.cpp b/Swift/Controllers/AdHocManager.cpp
new file mode 100644
index 0000000..368771f
--- /dev/null
+++ b/Swift/Controllers/AdHocManager.cpp
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2010-2011 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include <Swift/Controllers/AdHocManager.h>
+
+#include <boost/bind.hpp>
+#include <boost/shared_ptr.hpp>
+
+#include <Swiften/Base/foreach.h>
+#include <Swiften/Queries/IQRouter.h>
+#include <Swiften/Disco/GetDiscoItemsRequest.h>
+#include <Swiften/AdHoc/OutgoingAdHocCommandSession.h>
+#include <Swift/Controllers/UIInterfaces/MainWindow.h>
+#include <Swift/Controllers/UIInterfaces/AdHocCommandWindowFactory.h>
+#include <Swift/Controllers/UIEvents/UIEventStream.h>
+#include <Swift/Controllers/UIEvents/RequestAdHocUIEvent.h>
+
+namespace Swift {
+
+AdHocManager::AdHocManager(const JID& jid, AdHocCommandWindowFactory* factory, IQRouter* iqRouter, UIEventStream* uiEventStream, MainWindow* mainWindow) : jid_(jid) {
+	iqRouter_ = iqRouter;
+	uiEventStream_ = uiEventStream;
+	mainWindow_ = mainWindow;
+	factory_ = factory;
+
+	uiEventStream_->onUIEvent.connect(boost::bind(&AdHocManager::handleUIEvent, this, _1));
+}
+
+AdHocManager::~AdHocManager() {
+
+}
+
+void AdHocManager::setServerDiscoInfo(boost::shared_ptr<DiscoInfo> info) {
+	if (iqRouter_->isAvailable() && info->hasFeature(DiscoInfo::CommandsFeature)) {
+		GetDiscoItemsRequest::ref discoItemsRequest = GetDiscoItemsRequest::create(JID(jid_.getDomain()), DiscoInfo::CommandsFeature, iqRouter_);
+		discoItemsRequest->onResponse.connect(boost::bind(&AdHocManager::handleServerDiscoItemsResponse, this, _1, _2));
+		discoItemsRequest->send();
+	} else {
+		mainWindow_->setAvailableAdHocCommands(std::vector<DiscoItems::Item>());
+	}
+
+}
+
+void AdHocManager::handleServerDiscoItemsResponse(boost::shared_ptr<DiscoItems> items, ErrorPayload::ref error) {
+	std::vector<DiscoItems::Item> commands;
+	if (!error) {
+		foreach (DiscoItems::Item item, items->getItems()) {
+			if (item.getNode() != "http://isode.com/xmpp/commands#test") {
+				commands.push_back(item);
+			}
+		}
+	}
+	mainWindow_->setAvailableAdHocCommands(commands);
+}
+
+void AdHocManager::handleUIEvent(boost::shared_ptr<UIEvent> event) {
+	boost::shared_ptr<RequestAdHocUIEvent> adHocEvent = boost::dynamic_pointer_cast<RequestAdHocUIEvent>(event);
+	if (adHocEvent) {
+		factory_->createAdHocCommandWindow(boost::shared_ptr<OutgoingAdHocCommandSession>(new OutgoingAdHocCommandSession(adHocEvent->getCommand(), factory_, iqRouter_)));
+	}
+}
+
+}
diff --git a/Swift/Controllers/AdHocManager.h b/Swift/Controllers/AdHocManager.h
new file mode 100644
index 0000000..9e21bf7
--- /dev/null
+++ b/Swift/Controllers/AdHocManager.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2010-2011 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include <boost/shared_ptr.hpp>
+
+#include <Swiften/Base/boost_bsignals.h>
+#include <Swiften/JID/JID.h>
+#include <Swiften/Elements/DiscoInfo.h>
+#include <Swiften/Elements/DiscoItems.h>
+#include <Swiften/Elements/ErrorPayload.h>
+#include <Swift/Controllers/UIEvents/UIEvent.h>
+
+namespace Swift {
+	class IQRouter;
+	class MainWindow;
+	class UIEventStream;
+	class AdHocCommandWindowFactory;
+	class AdHocManager {
+		public:
+			AdHocManager(const JID& jid, AdHocCommandWindowFactory* factory, IQRouter* iqRouter, UIEventStream* uiEventStream, MainWindow* mainWindow);
+			~AdHocManager();
+			void setServerDiscoInfo(boost::shared_ptr<DiscoInfo> info);
+		private:
+			void handleUIEvent(boost::shared_ptr<UIEvent> event);
+			void handleServerDiscoItemsResponse(boost::shared_ptr<DiscoItems>, ErrorPayload::ref error);
+			JID jid_;
+			IQRouter* iqRouter_;
+			UIEventStream* uiEventStream_;
+			MainWindow* mainWindow_;
+			AdHocCommandWindowFactory* factory_;
+	};
+}
diff --git a/Swift/Controllers/MainController.cpp b/Swift/Controllers/MainController.cpp
index 3f86b43..efe4de4 100644
--- a/Swift/Controllers/MainController.cpp
+++ b/Swift/Controllers/MainController.cpp
@@ -21,7 +21,6 @@
 #include "Swift/Controllers/StoragesFactory.h"
 #include "Swiften/Client/Storages.h"
 #include "Swiften/VCards/VCardManager.h"
-#include "Swift/Controllers/Chat/MUCSearchController.h"
 #include "Swift/Controllers/Chat/UserSearchController.h"
 #include "Swift/Controllers/Chat/ChatsManager.h"
 #include "Swift/Controllers/XMPPEvents/EventController.h"
@@ -66,6 +65,7 @@
 #include <Swift/Controllers/ProfileController.h>
 #include <Swift/Controllers/ContactEditController.h>
 #include <Swift/Controllers/XMPPURIController.h>
+#include "Swift/Controllers/AdHocManager.h"
 
 namespace Swift {
 
@@ -268,14 +268,14 @@ void MainController::handleConnected() {
 		client_->getDiscoManager()->setCapsNode(CLIENT_NODE);
 		client_->getDiscoManager()->setDiscoInfo(discoInfo);
 
-
 		userSearchControllerChat_ = new UserSearchController(UserSearchController::StartChat, jid_, uiEventStream_, uiFactory_, client_->getIQRouter(), rosterController_);
 		userSearchControllerAdd_ = new UserSearchController(UserSearchController::AddContact, jid_, uiEventStream_, uiFactory_, client_->getIQRouter(), rosterController_);
+		adHocManager_ = new AdHocManager(boundJID_, uiFactory_, client_->getIQRouter(), uiEventStream_, rosterController_->getWindow());
 	}
 	
 	client_->requestRoster();
 
-	GetDiscoInfoRequest::ref discoInfoRequest = GetDiscoInfoRequest::create(JID(), client_->getIQRouter());
+	GetDiscoInfoRequest::ref discoInfoRequest = GetDiscoInfoRequest::create(boundJID_.toBare(), client_->getIQRouter());
 	discoInfoRequest->onResponse.connect(boost::bind(&MainController::handleServerDiscoInfoResponse, this, _1, _2));
 	discoInfoRequest->send();
 
@@ -560,6 +560,7 @@ void MainController::setManagersOffline() {
 void MainController::handleServerDiscoInfoResponse(boost::shared_ptr<DiscoInfo> info, ErrorPayload::ref error) {
 	if (!error) {
 		chatsManager_->setServerDiscoInfo(info);
+		adHocManager_->setServerDiscoInfo(info);
 	}
 }
 
diff --git a/Swift/Controllers/MainController.h b/Swift/Controllers/MainController.h
index f9722de..ae69e70 100644
--- a/Swift/Controllers/MainController.h
+++ b/Swift/Controllers/MainController.h
@@ -64,6 +64,8 @@ namespace Swift {
 	class NetworkFactories;
 	class URIHandler;
 	class XMPPURIController;
+	class AdHocManager;
+	class AdHocCommandWindowFactory;
 
 	class MainController {
 		public:
@@ -133,6 +135,7 @@ namespace Swift {
 			RosterController* rosterController_;
 			EventController* eventController_;
 			EventWindowController* eventWindowController_;
+			AdHocManager* adHocManager_;
 			LoginWindow* loginWindow_;
 			UIEventStream* uiEventStream_;
 			XMLConsoleController* xmlConsoleController_;
diff --git a/Swift/Controllers/SConscript b/Swift/Controllers/SConscript
index eb5d21f..bd18a3e 100644
--- a/Swift/Controllers/SConscript
+++ b/Swift/Controllers/SConscript
@@ -44,6 +44,7 @@ if env["SCONS_STAGE"] == "build" :
 			"StatusTracker.cpp",
 			"PresenceNotifier.cpp",
 			"EventNotifier.cpp",
+			"AdHocManager.cpp",
 			"XMPPEvents/EventController.cpp",
 			"UIEvents/UIEvent.cpp",
 			"UIInterfaces/XMLConsoleWidget.cpp",
diff --git a/Swift/Controllers/UIEvents/RequestAdHocUIEvent.h b/Swift/Controllers/UIEvents/RequestAdHocUIEvent.h
new file mode 100644
index 0000000..c3b4b49
--- /dev/null
+++ b/Swift/Controllers/UIEvents/RequestAdHocUIEvent.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include <Swift/Controllers/UIInterfaces/MainWindow.h>
+
+#include <Swift/Controllers/UIEvents/UIEvent.h>
+
+namespace Swift {
+	class RequestAdHocUIEvent : public UIEvent {
+		public:
+			RequestAdHocUIEvent(const DiscoItems::Item& command) : command_(command) {};
+			const DiscoItems::Item& getCommand() const {return command_;}
+		private:
+			DiscoItems::Item command_;
+	};
+}
diff --git a/Swift/Controllers/UIInterfaces/AdHocCommandWindow.h b/Swift/Controllers/UIInterfaces/AdHocCommandWindow.h
new file mode 100644
index 0000000..f7a5d39
--- /dev/null
+++ b/Swift/Controllers/UIInterfaces/AdHocCommandWindow.h
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2010 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+namespace Swift {
+	class AdHocCommandWindow {
+		public:
+			virtual ~AdHocCommandWindow() {};
+	};
+}
diff --git a/Swift/Controllers/UIInterfaces/AdHocCommandWindowFactory.h b/Swift/Controllers/UIInterfaces/AdHocCommandWindowFactory.h
new file mode 100644
index 0000000..ae77180
--- /dev/null
+++ b/Swift/Controllers/UIInterfaces/AdHocCommandWindowFactory.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2010-2011 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include <Swift/Controllers/UIInterfaces/AdHocCommandWindow.h>
+#include <Swiften/AdHoc/OutgoingAdHocCommandSession.h>
+
+namespace Swift {
+	class AdHocCommandWindowFactory {
+		public:
+			virtual ~AdHocCommandWindowFactory() {}
+			/**
+			 * The UI should deal with the lifetime of this window (i.e. DeleteOnClose),
+			 * so the result isn't returned.
+			 */
+			virtual void createAdHocCommandWindow(boost::shared_ptr<OutgoingAdHocCommandSession> command) = 0;
+	};
+}
diff --git a/Swift/Controllers/UIInterfaces/MainWindow.h b/Swift/Controllers/UIInterfaces/MainWindow.h
index 2fd463b..93584e7 100644
--- a/Swift/Controllers/UIInterfaces/MainWindow.h
+++ b/Swift/Controllers/UIInterfaces/MainWindow.h
@@ -9,6 +9,7 @@
 #include <string>
 #include "Swiften/JID/JID.h"
 #include "Swiften/Elements/StatusShow.h"
+#include "Swiften/Elements/DiscoItems.h"
 
 #include "Swiften/Base/boost_bsignals.h"
 #include <boost/shared_ptr.hpp>
@@ -33,6 +34,7 @@ namespace Swift {
 			/** Must be able to cope with NULL to clear the roster */
 			virtual void setRosterModel(Roster* roster) = 0;
 			virtual void setConnecting() = 0;
+			virtual void setAvailableAdHocCommands(const std::vector<DiscoItems::Item>& commands) = 0;
 			
 			boost::signal<void (StatusShow::Type, const std::string&)> onChangeStatusRequest;
 			boost::signal<void ()> onSignOutRequest;
diff --git a/Swift/Controllers/UIInterfaces/UIFactory.h b/Swift/Controllers/UIInterfaces/UIFactory.h
index 9b36ac5..57f55d0 100644
--- a/Swift/Controllers/UIInterfaces/UIFactory.h
+++ b/Swift/Controllers/UIInterfaces/UIFactory.h
@@ -17,6 +17,7 @@
 #include <Swift/Controllers/UIInterfaces/XMLConsoleWidgetFactory.h>
 #include <Swift/Controllers/UIInterfaces/ProfileWindowFactory.h>
 #include <Swift/Controllers/UIInterfaces/ContactEditWindowFactory.h>
+#include <Swift/Controllers/UIInterfaces/AdHocCommandWindowFactory.h>
 
 namespace Swift {
 	class UIFactory : 
@@ -30,7 +31,8 @@ namespace Swift {
 			public UserSearchWindowFactory, 
 			public JoinMUCWindowFactory,
 			public ProfileWindowFactory,
-			public ContactEditWindowFactory {
+			public ContactEditWindowFactory,
+			public AdHocCommandWindowFactory {
 		public:
 			virtual ~UIFactory() {}
 	};
diff --git a/Swift/Controllers/UnitTest/MockMainWindow.h b/Swift/Controllers/UnitTest/MockMainWindow.h
index afa5c2a..f773062 100644
--- a/Swift/Controllers/UnitTest/MockMainWindow.h
+++ b/Swift/Controllers/UnitTest/MockMainWindow.h
@@ -20,6 +20,7 @@ namespace Swift {
 			virtual void setMyAvatarPath(const std::string& /*path*/) {};
 			virtual void setMyStatusText(const std::string& /*status*/) {};
 			virtual void setMyStatusType(StatusShow::Type /*type*/) {};
+			virtual void setAvailableAdHocCommands(const std::vector<DiscoItems::Item>& /*commands*/) {};
 			virtual void setConnecting() {};
 			Roster* roster;
 
diff --git a/Swift/QtUI/QtAdHocCommandWindow.cpp b/Swift/QtUI/QtAdHocCommandWindow.cpp
new file mode 100644
index 0000000..f6b1b5c
--- /dev/null
+++ b/Swift/QtUI/QtAdHocCommandWindow.cpp
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2010-2011 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include <Swift/QtUI/QtAdHocCommandWindow.h>
+
+#include <boost/bind.hpp>
+#include <QBoxLayout>
+#include <Swift/QtUI/QtFormWidget.h>
+
+namespace Swift {
+QtAdHocCommandWindow::QtAdHocCommandWindow(boost::shared_ptr<OutgoingAdHocCommandSession> command) : command_(command) {
+
+	formWidget_ = NULL;
+
+	setAttribute(Qt::WA_DeleteOnClose);
+	command->onNextStageReceived.connect(boost::bind(&QtAdHocCommandWindow::handleNextStageReceived, this, _1));
+	command->onError.connect(boost::bind(&QtAdHocCommandWindow::handleError, this, _1));
+	command->start();
+
+	QBoxLayout* layout = new QBoxLayout(QBoxLayout::TopToBottom, this);
+	layout->setContentsMargins(0,0,0,0);
+	layout->setSpacing(2);
+	label_ = new QLabel(this);
+	layout->addWidget(label_);
+	QWidget* formContainer = new QWidget(this);
+	layout->addWidget(formContainer);
+	formLayout_ = new QBoxLayout(QBoxLayout::TopToBottom, formContainer);
+	QWidget* buttonsWidget = new QWidget(this);
+	layout->addWidget(buttonsWidget);
+
+	QBoxLayout* buttonsLayout = new QBoxLayout(QBoxLayout::LeftToRight, buttonsWidget);
+	cancelButton_ = new QPushButton(tr("Cancel"), buttonsWidget);
+	buttonsLayout->addWidget(cancelButton_);
+	connect(cancelButton_, SIGNAL(clicked()), this, SLOT(handleCancelClicked()));
+	backButton_ = new QPushButton(tr("Back"), buttonsWidget);
+	buttonsLayout->addWidget(backButton_);
+	connect(backButton_, SIGNAL(clicked()), this, SLOT(handlePrevClicked()));
+	nextButton_ = new QPushButton(tr("Next"), buttonsWidget);
+	buttonsLayout->addWidget(nextButton_);
+	connect(nextButton_, SIGNAL(clicked()), this, SLOT(handleNextClicked()));
+	completeButton_ = new QPushButton(tr("Complete"), buttonsWidget);
+	buttonsLayout->addWidget(completeButton_);
+	connect(completeButton_, SIGNAL(clicked()), this, SLOT(handleCompleteClicked()));
+	nextButton_->setEnabled(false);
+	backButton_->setEnabled(false);
+	completeButton_->setEnabled(false);
+	show();
+}
+
+QtAdHocCommandWindow::~QtAdHocCommandWindow() {
+
+}
+
+void QtAdHocCommandWindow::handleCancelClicked() {
+	command_->cancel();
+}
+
+void QtAdHocCommandWindow::handlePrevClicked() {
+	command_->goBack();
+}
+
+void QtAdHocCommandWindow::handleNextClicked() {
+	command_->goNext(formWidget_ ? formWidget_->getCompletedForm() : Form::ref());
+}
+
+void QtAdHocCommandWindow::handleCompleteClicked() {
+	command_->complete(formWidget_ ? formWidget_->getCompletedForm() : Form::ref());
+}
+
+void QtAdHocCommandWindow::handleNextStageReceived(Command::ref command) {
+	if (command->getForm()) {
+		setForm(command->getForm());
+	} else {
+		setNoForm();
+	}
+	QString notes;
+	foreach (Command::Note note, command->getNotes()) {
+		if (!notes.isEmpty()) {
+			notes += "\n";
+			QString qNote(note.note.c_str());
+			switch (note.type) {
+				case Command::Note::Error: notes += tr("Error: %1").arg(qNote); break;
+				case Command::Note::Warn: notes += tr("Warning: %1").arg(qNote); break;
+				case Command::Note::Info: notes += qNote; break;
+			}
+		}
+	}
+	label_->setText(notes);
+	setAvailableActions(command);
+}
+
+void QtAdHocCommandWindow::handleError(ErrorPayload::ref /*error*/) {
+	nextButton_->setEnabled(false);
+	backButton_->setEnabled(false);
+	completeButton_->setEnabled(false);
+	label_->setText(tr("Error executing command"));
+}
+
+void QtAdHocCommandWindow::setForm(Form::ref form) {
+	delete formWidget_;
+	formWidget_ = new QtFormWidget(form, this);
+	formLayout_->addWidget(formWidget_);
+}
+
+void QtAdHocCommandWindow::setNoForm() {
+	delete formWidget_;
+}
+
+void QtAdHocCommandWindow::setAvailableActions(Command::ref commandResult) {
+	const std::vector<Command::Action> actions = commandResult->getAvailableActions();
+	nextButton_->setEnabled(std::find(actions.begin(), actions.end(), Command::Next) != actions.end());
+	backButton_->setEnabled(std::find(actions.begin(), actions.end(), Command::Prev) != actions.end());
+	completeButton_->setEnabled(std::find(actions.begin(), actions.end(), Command::Complete) != actions.end());
+
+
+	if (command_->getIsMultiStage()) {
+		backButton_->show();
+		nextButton_->show();
+	}
+	else {
+		backButton_->hide();
+		nextButton_->hide();
+
+	}
+}
+
+}
diff --git a/Swift/QtUI/QtAdHocCommandWindow.h b/Swift/QtUI/QtAdHocCommandWindow.h
new file mode 100644
index 0000000..f1073bc
--- /dev/null
+++ b/Swift/QtUI/QtAdHocCommandWindow.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2010-2011 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include <QWidget>
+#include <QPushButton>
+#include <QLabel>
+
+#include <Swift/Controllers/UIInterfaces/AdHocCommandWindow.h>
+#include <Swiften/AdHoc/OutgoingAdHocCommandSession.h>
+
+class QBoxLayout;
+
+namespace Swift {
+	class QtFormWidget;
+	class QtAdHocCommandWindow : public QWidget, public AdHocCommandWindow {
+		Q_OBJECT
+		public:
+			QtAdHocCommandWindow(boost::shared_ptr<OutgoingAdHocCommandSession> command);
+			virtual ~QtAdHocCommandWindow();
+		private:
+			void handleNextStageReceived(Command::ref command);
+			void handleError(ErrorPayload::ref error);
+			void setForm(Form::ref);
+			void setNoForm();
+			void setAvailableActions(Command::ref commandResult);
+		private slots:
+			void handleCancelClicked();
+			void handlePrevClicked();
+			void handleNextClicked();
+			void handleCompleteClicked();
+		private:
+			boost::shared_ptr<OutgoingAdHocCommandSession> command_;
+			QtFormWidget* formWidget_;
+			QBoxLayout* formLayout_;
+			Form::ref form_;
+			QLabel* label_;
+			QPushButton* backButton_;
+			QPushButton* nextButton_;
+			QPushButton* completeButton_;
+			QPushButton* cancelButton_;
+	};
+}
diff --git a/Swift/QtUI/QtFormWidget.cpp b/Swift/QtUI/QtFormWidget.cpp
new file mode 100644
index 0000000..659fb25
--- /dev/null
+++ b/Swift/QtUI/QtFormWidget.cpp
@@ -0,0 +1,248 @@
+/*
+ * Copyright (c) 2010-2011 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include <Swift/QtUI/QtFormWidget.h>
+
+#include <QGridLayout>
+#include <QLabel>
+#include <QListWidget>
+#include <QLineEdit>
+#include <QTextEdit>
+#include <QCheckBox>
+#include <QScrollArea>
+
+#include <Swift/QtUI/QtSwiftUtil.h>
+#include <Swiften/Base/foreach.h>
+
+namespace Swift {
+
+QtFormWidget::QtFormWidget(Form::ref form, QWidget* parent) : QWidget(parent), form_(form) {
+	QGridLayout* thisLayout = new QGridLayout(this);
+	int row = 0;
+	if (!form->getTitle().empty()) {
+		QLabel* instructions = new QLabel(("<b>" + form->getTitle() + "</b>").c_str(), this);
+		thisLayout->addWidget(instructions, row++, 0, 1, 2);
+	}
+	if (!form->getInstructions().empty()) {
+		QLabel* instructions = new QLabel(form->getInstructions().c_str(), this);
+		thisLayout->addWidget(instructions, row++, 0, 1, 2);
+	}
+	QScrollArea* scrollArea = new QScrollArea(this);
+	thisLayout->addWidget(scrollArea);
+	QWidget* scroll = new QWidget(this);
+	QGridLayout* layout = new QGridLayout(scroll);
+	foreach (boost::shared_ptr<FormField> field, form->getFields()) {
+		QWidget* widget = createWidget(field);
+		if (widget) {
+			layout->addWidget(new QLabel(field->getLabel().c_str(), this), row, 0);
+			layout->addWidget(widget, row++, 1);
+		}
+	}
+	scrollArea->setWidget(scroll);
+	scrollArea->setWidgetResizable(true);
+}
+
+QtFormWidget::~QtFormWidget() {
+
+}
+
+QListWidget* QtFormWidget::createList(FormField::ref field) {
+	QListWidget* listWidget = new QListWidget(this);
+	listWidget->setSortingEnabled(false);
+	listWidget->setSelectionMode(boost::dynamic_pointer_cast<ListMultiFormField>(field) ? QAbstractItemView::MultiSelection : QAbstractItemView::SingleSelection);
+	foreach (FormField::Option option, field->getOptions()) {
+		listWidget->addItem(option.label.c_str());
+	}
+	boost::shared_ptr<ListMultiFormField> listMultiField = boost::dynamic_pointer_cast<ListMultiFormField>(field);
+	boost::shared_ptr<ListSingleFormField> listSingleField = boost::dynamic_pointer_cast<ListSingleFormField>(field);
+	for (int i = 0; i < listWidget->count(); i++) {
+		QListWidgetItem* item = listWidget->item(i);
+		bool selected = false;
+		if (listSingleField) {
+			selected = (item->text() == QString(listSingleField->getValue().c_str()));
+		}
+		else if (listMultiField) {
+			std::string text = Q2PSTRING(item->text());
+			selected = (std::find(listMultiField->getValue().begin(), listMultiField->getValue().end(), text) != listMultiField->getValue().end());
+		}
+		item->setSelected(selected);
+	}
+	return listWidget;
+}
+
+QWidget* QtFormWidget::createWidget(FormField::ref field) {
+	QWidget* widget = NULL;
+	boost::shared_ptr<BooleanFormField> booleanField = boost::dynamic_pointer_cast<BooleanFormField>(field);
+	if (booleanField) {
+		QCheckBox* checkWidget = new QCheckBox(this);
+		checkWidget->setCheckState(booleanField->getValue() ? Qt::Checked : Qt::Unchecked);
+		widget = checkWidget;
+	}
+	boost::shared_ptr<FixedFormField> fixedField = boost::dynamic_pointer_cast<FixedFormField>(field);
+	if (fixedField) {
+		QString value = fixedField->getValue().c_str();
+		widget = new QLabel(value, this);
+	}
+	boost::shared_ptr<ListSingleFormField> listSingleField = boost::dynamic_pointer_cast<ListSingleFormField>(field);
+	if (listSingleField) {
+		widget = createList(field);
+	}
+	boost::shared_ptr<TextMultiFormField> textMultiField = boost::dynamic_pointer_cast<TextMultiFormField>(field);
+	if (textMultiField) {
+		QString value = textMultiField->getValue().c_str();
+		widget = new QTextEdit(value, this);
+	}
+	boost::shared_ptr<TextPrivateFormField> textPrivateField = boost::dynamic_pointer_cast<TextPrivateFormField>(field);
+	if (textPrivateField) {
+		QString value = textPrivateField->getValue().c_str();
+		QLineEdit* lineWidget = new QLineEdit(value, this);
+		lineWidget->setEchoMode(QLineEdit::Password);
+		widget = lineWidget;
+	}
+	boost::shared_ptr<TextSingleFormField> textSingleField = boost::dynamic_pointer_cast<TextSingleFormField>(field);
+	if (textSingleField) {
+		QString value = textSingleField->getValue().c_str();
+		widget = new QLineEdit(value, this);
+	}
+	boost::shared_ptr<JIDSingleFormField> jidSingleField = boost::dynamic_pointer_cast<JIDSingleFormField>(field);
+	if (jidSingleField) {
+		QString value = jidSingleField->getValue().toString().c_str();
+		widget = new QLineEdit(value, this);
+	}
+	boost::shared_ptr<JIDMultiFormField> jidMultiField = boost::dynamic_pointer_cast<JIDMultiFormField>(field);
+	if (jidMultiField) {
+		QString text;
+		bool prev = false;
+		foreach (JID line, jidMultiField->getValue()) {
+			if (prev) {
+				text += "\n";
+			}
+			prev = true;
+			text += line.toString().c_str();
+		}
+		widget = new QTextEdit(text, this);
+	}
+	boost::shared_ptr<ListMultiFormField> listMultiField = boost::dynamic_pointer_cast<ListMultiFormField>(field);
+	if (listMultiField) {
+		widget = createList(field);
+	}
+	boost::shared_ptr<UntypedFormField> untypedField = boost::dynamic_pointer_cast<UntypedFormField>(field);
+	if (untypedField) {
+	}
+	boost::shared_ptr<HiddenFormField> hiddenField = boost::dynamic_pointer_cast<HiddenFormField>(field);
+	if (hiddenField) {
+	}
+	fields_[field->getName()] = widget;
+	return widget;
+}
+
+Form::ref QtFormWidget::getCompletedForm() {
+	Form::ref result(new Form(Form::SubmitType));
+	foreach (boost::shared_ptr<FormField> field, form_->getFields()) {
+		boost::shared_ptr<FormField> resultField;
+		boost::shared_ptr<BooleanFormField> booleanField = boost::dynamic_pointer_cast<BooleanFormField>(field);
+		if (booleanField) {
+			resultField = FormField::ref(BooleanFormField::create(qobject_cast<QCheckBox*>(fields_[field->getName()])->checkState() == Qt::Checked));
+		}
+		boost::shared_ptr<FixedFormField> fixedField = boost::dynamic_pointer_cast<FixedFormField>(field);
+		if (fixedField) {
+			resultField = FormField::ref(FixedFormField::create(fixedField->getValue()));
+		}
+		boost::shared_ptr<ListSingleFormField> listSingleField = boost::dynamic_pointer_cast<ListSingleFormField>(field);
+		if (listSingleField) {
+			QListWidget* listWidget = qobject_cast<QListWidget*>(fields_[field->getName()]);
+			if (listWidget->selectedItems().size() > 0) {
+				int i = listWidget->row(listWidget->selectedItems()[0]);
+				resultField = FormField::ref(ListSingleFormField::create(field->getOptions()[i].value));
+			}
+			else {
+				resultField = FormField::ref(ListSingleFormField::create());
+			}
+		}
+		boost::shared_ptr<TextMultiFormField> textMultiField = boost::dynamic_pointer_cast<TextMultiFormField>(field);
+		if (textMultiField) {
+			QTextEdit* widget = qobject_cast<QTextEdit*>(fields_[field->getName()]);
+			QString string = widget->toPlainText();
+			if (string.isEmpty()) {
+				resultField = FormField::ref(TextMultiFormField::create());
+			}
+			else {
+				resultField = FormField::ref(TextMultiFormField::create(Q2PSTRING(string)));
+			}
+		}
+		boost::shared_ptr<TextPrivateFormField> textPrivateField = boost::dynamic_pointer_cast<TextPrivateFormField>(field);
+		if (textPrivateField) {
+			QLineEdit* widget = qobject_cast<QLineEdit*>(fields_[field->getName()]);
+			QString string = widget->text();
+			if (string.isEmpty()) {
+				resultField = FormField::ref(TextPrivateFormField::create());
+			}
+			else {
+				resultField = FormField::ref(TextPrivateFormField::create(Q2PSTRING(string)));
+			}
+		}
+		boost::shared_ptr<TextSingleFormField> textSingleField = boost::dynamic_pointer_cast<TextSingleFormField>(field);
+		if (textSingleField) {
+			QLineEdit* widget = qobject_cast<QLineEdit*>(fields_[field->getName()]);
+			QString string = widget->text();
+			if (string.isEmpty()) {
+				resultField = FormField::ref(TextSingleFormField::create());
+			}
+			else {
+				resultField = FormField::ref(TextSingleFormField::create(Q2PSTRING(string)));
+			}
+		}
+		boost::shared_ptr<JIDSingleFormField> jidSingleField = boost::dynamic_pointer_cast<JIDSingleFormField>(field);
+		if (jidSingleField) {
+			QLineEdit* widget = qobject_cast<QLineEdit*>(fields_[field->getName()]);
+			QString string = widget->text();
+			JID jid(Q2PSTRING(string));
+			if (string.isEmpty()) {
+				resultField = FormField::ref(JIDSingleFormField::create());
+			}
+			else {
+				resultField = FormField::ref(JIDSingleFormField::create(jid));
+			}
+		}
+		boost::shared_ptr<JIDMultiFormField> jidMultiField = boost::dynamic_pointer_cast<JIDMultiFormField>(field);
+		if (jidMultiField) {
+			QTextEdit* widget = qobject_cast<QTextEdit*>(fields_[field->getName()]);
+			QString string = widget->toPlainText();
+			if (string.isEmpty()) {
+				resultField = FormField::ref(JIDMultiFormField::create());
+			}
+			else {
+				QStringList lines = string.split("\n");
+				std::vector<JID> value;
+				foreach (QString line, lines) {
+					value.push_back(JID(Q2PSTRING(line)));
+				}
+				resultField = FormField::ref(JIDMultiFormField::create(value));
+			}
+		}
+		boost::shared_ptr<ListMultiFormField> listMultiField = boost::dynamic_pointer_cast<ListMultiFormField>(field);
+		if (listMultiField) {
+			QListWidget* listWidget = qobject_cast<QListWidget*>(fields_[field->getName()]);
+			std::vector<std::string> values;
+			foreach (QListWidgetItem* item, listWidget->selectedItems()) {
+				values.push_back(field->getOptions()[listWidget->row(item)].value);
+			}
+			resultField = FormField::ref(ListMultiFormField::create(values));
+		}
+		boost::shared_ptr<UntypedFormField> untypedField = boost::dynamic_pointer_cast<UntypedFormField>(field);
+		if (untypedField) {
+		}
+		boost::shared_ptr<HiddenFormField> hiddenField = boost::dynamic_pointer_cast<HiddenFormField>(field);
+		if (hiddenField) {
+			resultField = FormField::ref(HiddenFormField::create(hiddenField->getValue()));
+		}
+		resultField->setName(field->getName());
+		result->addField(resultField);
+	}
+	return result;
+}
+
+}
diff --git a/Swift/QtUI/QtFormWidget.h b/Swift/QtUI/QtFormWidget.h
new file mode 100644
index 0000000..2fb7b98
--- /dev/null
+++ b/Swift/QtUI/QtFormWidget.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2011 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include <QWidget>
+
+#include <map>
+#include <Swiften/Elements/Form.h>
+
+class QListWidget;
+
+namespace Swift {
+
+class QtFormWidget : public QWidget {
+	Q_OBJECT
+	public:
+		QtFormWidget(Form::ref form, QWidget* parent = NULL);
+		virtual ~QtFormWidget();
+		Form::ref getCompletedForm();
+	private:
+		QWidget* createWidget(FormField::ref field);
+		QListWidget* createList(FormField::ref field);
+		std::map<std::string, QWidget*> fields_;
+		Form::ref form_;
+};
+
+}
diff --git a/Swift/QtUI/QtMainWindow.cpp b/Swift/QtUI/QtMainWindow.cpp
index 6391961..f7dba58 100644
--- a/Swift/QtUI/QtMainWindow.cpp
+++ b/Swift/QtUI/QtMainWindow.cpp
@@ -30,6 +30,7 @@
 #include "Swift/Controllers/UIEvents/RequestProfileEditorUIEvent.h"
 #include "Swift/Controllers/UIEvents/JoinMUCUIEvent.h"
 #include "Swift/Controllers/UIEvents/ToggleShowOfflineUIEvent.h"
+#include "Swift/Controllers/UIEvents/RequestAdHocUIEvent.h"
 
 namespace Swift {
 
@@ -99,6 +100,8 @@ QtMainWindow::QtMainWindow(QtSettingsProvider* settings, UIEventStream* uiEventS
 	chatUserAction_ = new QAction(tr("Start &Chat"), this);
 	connect(chatUserAction_, SIGNAL(triggered(bool)), this, SLOT(handleChatUserActionTriggered(bool)));
 	actionsMenu->addAction(chatUserAction_);
+	serverAdHocMenu_ = new QMenu("Server Commands", this);
+	actionsMenu->addMenu(serverAdHocMenu_);
 	actionsMenu->addSeparator();
 	QAction* signOutAction = new QAction(tr("&Sign Out"), this);
 	connect(signOutAction, SIGNAL(triggered()), SLOT(handleSignOutAction()));
@@ -106,6 +109,12 @@ QtMainWindow::QtMainWindow(QtSettingsProvider* settings, UIEventStream* uiEventS
 
 	connect(treeWidget_, SIGNAL(onSomethingSelectedChanged(bool)), editUserAction_, SLOT(setEnabled(bool)));
 
+	setAvailableAdHocCommands(std::vector<DiscoItems::Item>());
+	QAction* adHocAction = new QAction("Populating commands..", this);
+	adHocAction->setEnabled(false);
+	serverAdHocMenu_->addAction(adHocAction);
+	serverAdHocCommandActions_.append(adHocAction);
+
 	lastOfflineState_ = false;
 	uiEventStream_->onUIEvent.connect(boost::bind(&QtMainWindow::handleUIEvent, this, _1));
 }
@@ -206,6 +215,33 @@ void QtMainWindow::setConnecting() {
 	meView_->setConnecting();
 }
 
+void QtMainWindow::handleAdHocActionTriggered(bool /*checked*/) {
+	QAction* action = qobject_cast<QAction*>(sender());
+	assert(action);
+	DiscoItems::Item command = serverAdHocCommands_[serverAdHocCommandActions_.indexOf(action)];
+	uiEventStream_->send(boost::shared_ptr<UIEvent>(new RequestAdHocUIEvent(command)));
+}
+
+void QtMainWindow::setAvailableAdHocCommands(const std::vector<DiscoItems::Item>& commands) {
+	serverAdHocCommands_ = commands;
+	foreach (QAction* action, serverAdHocCommandActions_) {
+		delete action;
+	}
+	serverAdHocMenu_->clear();
+	serverAdHocCommandActions_.clear();
+	foreach (DiscoItems::Item command, commands) {
+		QAction* action = new QAction(P2QSTRING(command.getName()), this);
+		connect(action, SIGNAL(triggered(bool)), this, SLOT(handleAdHocActionTriggered(bool)));
+		serverAdHocMenu_->addAction(action);
+		serverAdHocCommandActions_.append(action);
+	}
+	if (serverAdHocCommandActions_.isEmpty()) {
+		QAction* action = new QAction("No available commands", this);
+		action->setEnabled(false);
+		serverAdHocMenu_->addAction(action);
+		serverAdHocCommandActions_.append(action);
+	}
+}
 
 }
 
diff --git a/Swift/QtUI/QtMainWindow.h b/Swift/QtUI/QtMainWindow.h
index 3462bb0..e20d773 100644
--- a/Swift/QtUI/QtMainWindow.h
+++ b/Swift/QtUI/QtMainWindow.h
@@ -8,6 +8,7 @@
 
 #include <QWidget>
 #include <QMenu>
+#include <QList>
 #include "Swift/Controllers/UIInterfaces/MainWindow.h"
 #include "Swift/QtUI/QtRosterHeader.h"
 #include "Swift/QtUI/EventViewer/QtEventWindow.h"
@@ -20,7 +21,7 @@ class QLineEdit;
 class QPushButton;
 class QToolBar;
 class QAction;
-
+class QMenu;
 class QTabWidget;
 
 namespace Swift {
@@ -46,6 +47,7 @@ namespace Swift {
 			QtEventWindow* getEventWindow();
 			QtChatListWindow* getChatListWindow();
 			void setRosterModel(Roster* roster);
+			void setAvailableAdHocCommands(const std::vector<DiscoItems::Item>& commands);
 		private slots:
 			void handleStatusChanged(StatusShow::Type showType, const QString &statusMessage);
 			void handleUIEvent(boost::shared_ptr<UIEvent> event);
@@ -55,6 +57,7 @@ namespace Swift {
 			void handleEditProfileAction();
 			void handleAddUserActionTriggered(bool checked);
 			void handleChatUserActionTriggered(bool checked);
+			void handleAdHocActionTriggered(bool checked);
 			void handleEventCountUpdated(int count);
 			void handleEditProfileRequest();
 
@@ -66,6 +69,7 @@ namespace Swift {
 			QAction* editUserAction_;
 			QAction* chatUserAction_;
 			QAction* showOfflineAction_;
+			QMenu* serverAdHocMenu_;
 			QtTabWidget* tabs_;
 			QWidget* contactsTabWidget_;
 			QWidget* eventsTabWidget_;
@@ -73,5 +77,7 @@ namespace Swift {
 			QtChatListWindow* chatListWindow_;
 			UIEventStream* uiEventStream_;
 			bool lastOfflineState_;
+			std::vector<DiscoItems::Item> serverAdHocCommands_;
+			QList<QAction*> serverAdHocCommandActions_;
 	};
 }
diff --git a/Swift/QtUI/QtUIFactory.cpp b/Swift/QtUI/QtUIFactory.cpp
index 35fbfcd..5c1f78d 100644
--- a/Swift/QtUI/QtUIFactory.cpp
+++ b/Swift/QtUI/QtUIFactory.cpp
@@ -23,6 +23,7 @@
 #include "UserSearch/QtUserSearchWindow.h"
 #include "QtProfileWindow.h"
 #include "QtContactEditWindow.h"
+#include "QtAdHocCommandWindow.h"
 
 namespace Swift {
 
@@ -99,5 +100,8 @@ ContactEditWindow* QtUIFactory::createContactEditWindow() {
 	return new QtContactEditWindow();
 }
 
+void QtUIFactory::createAdHocCommandWindow(boost::shared_ptr<OutgoingAdHocCommandSession> command) {
+	new QtAdHocCommandWindow(command);
+}
 
 }
diff --git a/Swift/QtUI/QtUIFactory.h b/Swift/QtUI/QtUIFactory.h
index ddaaf6e..9ef228a 100644
--- a/Swift/QtUI/QtUIFactory.h
+++ b/Swift/QtUI/QtUIFactory.h
@@ -37,6 +37,7 @@ namespace Swift {
 			virtual JoinMUCWindow* createJoinMUCWindow();
 			virtual ProfileWindow* createProfileWindow();
 			virtual ContactEditWindow* createContactEditWindow();
+			virtual void createAdHocCommandWindow(boost::shared_ptr<OutgoingAdHocCommandSession> command);
 
 		private slots:
 			void handleLoginWindowGeometryChanged();
diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript
index 58f2b99..2f138cd 100644
--- a/Swift/QtUI/SConscript
+++ b/Swift/QtUI/SConscript
@@ -88,6 +88,7 @@ sources = [
     "QtTabWidget.cpp",
     "QtTextEdit.cpp",
     "QtXMLConsoleWidget.cpp",
+    "QtAdHocCommandWindow.cpp",
     "QtUtilities.cpp",
     "QtBookmarkDetailWindow.cpp",
     "QtAddBookmarkWindow.cpp",
@@ -98,6 +99,7 @@ sources = [
     "MessageSnippet.cpp",
     "SystemMessageSnippet.cpp",
     "QtElidingLabel.cpp",
+    "QtFormWidget.cpp",
     "QtLineEdit.cpp",
     "QtJoinMUCWindow.cpp",
     "Roster/RosterModel.cpp",
diff --git a/Swiften/AdHoc/OutgoingAdHocCommandSession.cpp b/Swiften/AdHoc/OutgoingAdHocCommandSession.cpp
new file mode 100644
index 0000000..40b17e7
--- /dev/null
+++ b/Swiften/AdHoc/OutgoingAdHocCommandSession.cpp
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2010-2011 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include <Swiften/AdHoc/OutgoingAdHocCommandSession.h>
+
+#include <boost/bind.hpp>
+
+#include <Swiften/Queries/GenericRequest.h>
+
+namespace Swift {
+OutgoingAdHocCommandSession::OutgoingAdHocCommandSession(const DiscoItems::Item& command, AdHocCommandWindowFactory* /*factory*/, IQRouter* iqRouter) : command_(command), iqRouter_(iqRouter), isMultiStage_(false) {
+
+}
+
+void OutgoingAdHocCommandSession::handleResponse(boost::shared_ptr<Command> payload, ErrorPayload::ref error) {
+	if (error) {
+		onError(error);
+	} else {
+		sessionID_ = payload->getSessionID();
+		const std::vector<Command::Action> actions = payload->getAvailableActions();
+		if (std::find(actions.begin(), actions.end(), Command::Next) != actions.end()
+				|| std::find(actions.begin(), actions.end(), Command::Prev) != actions.end()) {
+			isMultiStage_ = true;
+		}
+		onNextStageReceived(payload);
+	}
+}
+
+bool OutgoingAdHocCommandSession::getIsMultiStage() {
+	return isMultiStage_;
+}
+
+void OutgoingAdHocCommandSession::start() {
+	boost::shared_ptr<Payload> commandPayload(new Command(command_.getNode()));
+	boost::shared_ptr<GenericRequest<Command> > commandRequest(new GenericRequest<Command>(IQ::Set, command_.getJID(), commandPayload, iqRouter_));
+	commandRequest->onResponse.connect(boost::bind(&OutgoingAdHocCommandSession::handleResponse, this, _1, _2));
+	commandRequest->send();
+}
+
+void OutgoingAdHocCommandSession::cancel() {
+	if (!sessionID_.empty()) {
+		boost::shared_ptr<Payload> commandPayload(new Command(command_.getNode(), sessionID_, Command::Cancel));
+		boost::shared_ptr<GenericRequest<Command> > commandRequest(new GenericRequest<Command>(IQ::Set, command_.getJID(), commandPayload, iqRouter_));
+		commandRequest->onResponse.connect(boost::bind(&OutgoingAdHocCommandSession::handleResponse, this, _1, _2));
+		commandRequest->send();
+	}
+}
+
+void OutgoingAdHocCommandSession::goBack() {
+	boost::shared_ptr<Payload> commandPayload(new Command(command_.getNode(), sessionID_, Command::Prev));
+	boost::shared_ptr<GenericRequest<Command> > commandRequest(new GenericRequest<Command>(IQ::Set, command_.getJID(), commandPayload, iqRouter_));
+	commandRequest->onResponse.connect(boost::bind(&OutgoingAdHocCommandSession::handleResponse, this, _1, _2));
+	commandRequest->send();
+}
+
+void OutgoingAdHocCommandSession::complete(Form::ref form) {
+	Command* command = new Command(command_.getNode(), sessionID_, Command::Complete);
+	boost::shared_ptr<Payload> commandPayload(command);
+	command->setForm(form);
+	boost::shared_ptr<GenericRequest<Command> > commandRequest(new GenericRequest<Command>(IQ::Set, command_.getJID(), commandPayload, iqRouter_));
+	commandRequest->onResponse.connect(boost::bind(&OutgoingAdHocCommandSession::handleResponse, this, _1, _2));
+	commandRequest->send();
+}
+
+void OutgoingAdHocCommandSession::goNext(Form::ref form) {
+	Command* command = new Command(command_.getNode(), sessionID_, Command::Next);
+	boost::shared_ptr<Payload> commandPayload(command);
+	command->setForm(form);
+	boost::shared_ptr<GenericRequest<Command> > commandRequest(new GenericRequest<Command>(IQ::Set, command_.getJID(), commandPayload, iqRouter_));
+	commandRequest->onResponse.connect(boost::bind(&OutgoingAdHocCommandSession::handleResponse, this, _1, _2));
+	commandRequest->send();
+}
+
+}
diff --git a/Swiften/AdHoc/OutgoingAdHocCommandSession.h b/Swiften/AdHoc/OutgoingAdHocCommandSession.h
new file mode 100644
index 0000000..820dc62
--- /dev/null
+++ b/Swiften/AdHoc/OutgoingAdHocCommandSession.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2010-2011 Kevin Smith
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include <Swiften/Base/boost_bsignals.h>
+#include <boost/shared_ptr.hpp>
+#include <Swiften/Elements/DiscoItems.h>
+#include <Swiften/Elements/Command.h>
+#include <Swiften/Elements/ErrorPayload.h>
+
+namespace Swift {
+	class IQRouter;
+	class MainWindow;
+	class UIEventStream;
+	class AdHocCommandWindowFactory;
+	class OutgoingAdHocCommandSession {
+		public:
+			OutgoingAdHocCommandSession(const DiscoItems::Item& command, AdHocCommandWindowFactory* factory, IQRouter* iqRouter);
+			/**
+			 * Send initial request to the target.
+			 */
+			void start();
+			/**
+			 * Cancel command session with the target.
+			 */
+			void cancel();
+			/**
+			 * Return to the previous stage.
+			 */
+			void goBack();
+			/**
+			 * Send the form to complete the command.
+			 * \param form Form for submission - if missing the command will be submitted with no form.
+			 */
+			void complete(Form::ref form);
+			/**
+			 * Send the form to advance to the next stage of the command.
+			 * \param form Form for submission - if missing the command will be submitted with no form.
+			 */
+			void goNext(Form::ref form);
+
+			/**
+			 * Is the form multi-stage?
+			 */
+			bool getIsMultiStage();
+
+			/**
+			 * Emitted when the form for the next stage is available.
+			 */
+			boost::signal<void (Command::ref)> onNextStageReceived;
+
+			/**
+			 * Emitted on error.
+			 */
+			boost::signal<void (ErrorPayload::ref)> onError;
+		private:
+			void handleResponse(boost::shared_ptr<Command> payload, ErrorPayload::ref error);
+		private:
+			DiscoItems::Item command_;
+			IQRouter* iqRouter_;
+			bool isMultiStage_;
+			std::string sessionID_;
+	};
+}
diff --git a/Swiften/AdHoc/SConscript b/Swiften/AdHoc/SConscript
new file mode 100644
index 0000000..69c9083
--- /dev/null
+++ b/Swiften/AdHoc/SConscript
@@ -0,0 +1,6 @@
+Import("swiften_env")
+
+objects = swiften_env.SwiftenObject([
+			"OutgoingAdHocCommandSession.cpp",
+		])
+swiften_env.Append(SWIFTEN_OBJECTS = [objects])
diff --git a/Swiften/Disco/GetDiscoItemsRequest.h b/Swiften/Disco/GetDiscoItemsRequest.h
index 0a94402..46735ef 100644
--- a/Swiften/Disco/GetDiscoItemsRequest.h
+++ b/Swiften/Disco/GetDiscoItemsRequest.h
@@ -18,9 +18,18 @@ namespace Swift {
 				return ref(new GetDiscoItemsRequest(jid, router));
 			}
 
+			static ref create(const JID& jid, const std::string& node, IQRouter* router) {
+				return ref(new GetDiscoItemsRequest(jid, node, router));
+			}
+
 		private:
 			GetDiscoItemsRequest(const JID& jid, IQRouter* router) :
 					GenericRequest<DiscoItems>(IQ::Get, jid, boost::shared_ptr<DiscoItems>(new DiscoItems()), router) {
 			}
+
+			GetDiscoItemsRequest(const JID& jid, const std::string& node, IQRouter* router) :
+				GenericRequest<DiscoItems>(IQ::Get, jid, boost::shared_ptr<DiscoItems>(new DiscoItems()), router) {
+				getPayloadGeneric()->setNode(node);
+			}
 	};
 }
diff --git a/Swiften/Elements/DiscoInfo.cpp b/Swiften/Elements/DiscoInfo.cpp
index 158ea66..5b2bb04 100644
--- a/Swiften/Elements/DiscoInfo.cpp
+++ b/Swiften/Elements/DiscoInfo.cpp
@@ -14,6 +14,7 @@ const std::string DiscoInfo::ChatStatesFeature = std::string("http://jabber.org/
 const std::string DiscoInfo::SecurityLabelsFeature = std::string("urn:xmpp:sec-label:0");
 const std::string DiscoInfo::SecurityLabelsCatalogFeature = std::string("urn:xmpp:sec-label:catalog:2");
 const std::string DiscoInfo::JabberSearchFeature = std::string("jabber:iq:search");
+const std::string DiscoInfo::CommandsFeature = std::string("http://jabber.org/protocol/commands");
 
 
 bool DiscoInfo::Identity::operator<(const Identity& other) const {
diff --git a/Swiften/Elements/DiscoInfo.h b/Swiften/Elements/DiscoInfo.h
index 6144b16..cd650b9 100644
--- a/Swiften/Elements/DiscoInfo.h
+++ b/Swiften/Elements/DiscoInfo.h
@@ -21,6 +21,7 @@ namespace Swift {
 			static const std::string SecurityLabelsFeature;
 			static const std::string SecurityLabelsCatalogFeature;
 			static const std::string JabberSearchFeature;
+			static const std::string CommandsFeature;
 
 			class Identity {
 				public:
diff --git a/Swiften/SConscript b/Swiften/SConscript
index 7c1f70f..5536b3f 100644
--- a/Swiften/SConscript
+++ b/Swiften/SConscript
@@ -190,6 +190,7 @@ if env["SCONS_STAGE"] == "build" :
 			"StreamManagement",
 			"Component",
 			"Config",
+			"AdHoc"
 		])
 	SConscript(test_only = True, dirs = [
 			"QA",
diff --git a/Swiften/Serializer/PayloadSerializers/CommandSerializer.cpp b/Swiften/Serializer/PayloadSerializers/CommandSerializer.cpp
index 0fa45ce..12a38a8 100644
--- a/Swiften/Serializer/PayloadSerializers/CommandSerializer.cpp
+++ b/Swiften/Serializer/PayloadSerializers/CommandSerializer.cpp
@@ -21,7 +21,7 @@ CommandSerializer::CommandSerializer() {
 }
 
 std::string CommandSerializer::serializePayload(boost::shared_ptr<Command> command)	const {
-	XMLElement commandElement("command", "http://jabber.org/protocol/comands");
+	XMLElement commandElement("command", "http://jabber.org/protocol/commands");
 	commandElement.setAttribute("node", command->getNode());
 
 	if (!command->getSessionID().empty()) {
-- 
cgit v0.10.2-6-g49f6