From 6f31cc8a329d15767d54511edd14339ac3dfdd7a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Remko=20Tron=C3=A7on?= <git@el-tramo.be>
Date: Mon, 6 Sep 2010 17:48:27 +0200
Subject: Added support for Entity Capabilities.

Resolves: #94

diff --git a/Swift/Controllers/MainController.cpp b/Swift/Controllers/MainController.cpp
index d742656..f3bd6d5 100644
--- a/Swift/Controllers/MainController.cpp
+++ b/Swift/Controllers/MainController.cpp
@@ -53,8 +53,11 @@
 #include "Swiften/Disco/CapsInfoGenerator.h"
 #include "Swiften/Queries/Requests/GetDiscoInfoRequest.h"
 #include "Swiften/Queries/Requests/GetVCardRequest.h"
-#include "Swiften/Avatars/AvatarFileStorage.h"
+#include "Swiften/Avatars/AvatarStorage.h"
 #include "Swiften/Avatars/AvatarManager.h"
+#include "Swiften/Disco/CapsFileStorage.h"
+#include "Swiften/Disco/CapsManager.h"
+#include "Swiften/Disco/EntityCapsManager.h"
 #include "Swiften/StringCodecs/SHA1.h"
 #include "Swiften/StringCodecs/Hexify.h"
 
@@ -78,6 +81,7 @@ MainController::MainController(
 		ChatListWindowFactory* chatListWindowFactory,
 		MUCSearchWindowFactory* mucSearchWindowFactory,
 		AvatarStorage* avatarStorage,
+		CapsStorage* capsStorage,
 		VCardStorageFactory* vcardStorageFactory,
 		ApplicationMessageDisplay* applicationMessageDisplay,
 		bool useDelayForLatency) :
@@ -115,6 +119,7 @@ MainController::MainController(
 	uiEventStream_ = new UIEventStream();
 
 	avatarStorage_ = avatarStorage;
+	capsStorage_ = capsStorage;
 	eventController_ = new EventController();
 	eventController_->onEventQueueLengthChange.connect(boost::bind(&MainController::handleEventQueueLengthChange, this, _1));
 
@@ -178,6 +183,10 @@ void MainController::resetClient() {
 	presenceOracle_ = NULL;
 	delete nickResolver_;
 	nickResolver_ = NULL;
+	delete entityCapsManager_;
+	entityCapsManager_ = NULL;
+	delete capsManager_;
+	capsManager_ = NULL;
 	delete avatarManager_;
 	avatarManager_ = NULL;
 	delete vcardManager_;
@@ -235,6 +244,8 @@ void MainController::handleConnected() {
 		vcardManager_ = new VCardManager(jid_, client_, getVCardStorageForProfile(jid_));
 		vcardManager_->onVCardChanged.connect(boost::bind(&MainController::handleVCardReceived, this, _1, _2));
 		avatarManager_ = new AvatarManager(vcardManager_, client_, avatarStorage_, mucRegistry_);
+		capsManager_ = new CapsManager(capsStorage_, client_, client_);
+		entityCapsManager_ = new EntityCapsManager(capsManager_, client_);
 
 		nickResolver_ = new NickResolver(this->jid_.toBare(), xmppRoster_, vcardManager_, mucRegistry_);
 
diff --git a/Swift/Controllers/MainController.h b/Swift/Controllers/MainController.h
index ef9c79e..8596ace 100644
--- a/Swift/Controllers/MainController.h
+++ b/Swift/Controllers/MainController.h
@@ -29,6 +29,7 @@
 
 namespace Swift {
 	class AvatarStorage;
+	class CapsStorage;
 	class VCardStorage;
 	class VCardManager;
 	class Application;
@@ -47,6 +48,8 @@ namespace Swift {
 	class DiscoInfoResponder;
 	class ApplicationMessageDisplay;
 	class AvatarManager;
+	class CapsManager;
+	class EntityCapsManager;
 	class LoginWindow;
 	class EventLoop;
 	class SoftwareVersionResponder;
@@ -82,6 +85,7 @@ namespace Swift {
 					ChatListWindowFactory* chatListWindowFactory_,
 					MUCSearchWindowFactory* mucSearchWindowFactory,
 					AvatarStorage* avatarStorage,
+					CapsStorage* capsStorage,
 					VCardStorageFactory* vcardStorageFactory,
 					ApplicationMessageDisplay* applicationMessageDisplay,
 					bool useDelayForLatency);
@@ -126,6 +130,7 @@ namespace Swift {
 			SettingsProvider *settings_;
 			ProfileSettingsProvider* profileSettings_;
 			AvatarStorage* avatarStorage_;
+			CapsStorage* capsStorage_;
 			VCardStorageFactory* vcardStorageFactory_;
 			VCardManager* vcardManager_;
 			ApplicationMessageDisplay* applicationMessageDisplay_;
@@ -149,6 +154,8 @@ namespace Swift {
 			SystemTrayController* systemTrayController_;
 			SoundEventController* soundEventController_;
 			AvatarManager* avatarManager_;
+			CapsManager* capsManager_;
+			EntityCapsManager* entityCapsManager_;
 			String vCardPhotoHash_;
 			String password_;
 			String certificateFile_;
diff --git a/Swift/QtUI/QtSwift.cpp b/Swift/QtUI/QtSwift.cpp
index e1f5e89..e548e29 100644
--- a/Swift/QtUI/QtSwift.cpp
+++ b/Swift/QtUI/QtSwift.cpp
@@ -27,6 +27,7 @@
 #include "Swiften/Application/PlatformApplication.h"
 #include "Swiften/Application/PlatformApplicationPathProvider.h"
 #include "Swiften/Avatars/AvatarFileStorage.h"
+#include "Swiften/Disco/CapsFileStorage.h"
 #include "Swiften/VCards/VCardFileStorageFactory.h"
 #include "Swiften/Base/String.h"
 #include "Swiften/Base/Platform.h"
@@ -84,6 +85,7 @@ QtSwift::QtSwift(po::variables_map options) : autoUpdater_(NULL) {
 	applicationPathProvider_ = new PlatformApplicationPathProvider(SWIFT_APPLICATION_NAME);
 	avatarStorage_ = new AvatarFileStorage(applicationPathProvider_->getAvatarDir());
 	vcardStorageFactory_ = new VCardFileStorageFactory(applicationPathProvider_->getDataDir());
+	capsStorage_ = new CapsFileStorage(applicationPathProvider_->getDataDir() / "caps");
 	chatWindowFactory_ = new QtChatWindowFactory(splitter_, settings_, tabs_, "");
 	soundPlayer_ = new QtSoundPlayer(applicationPathProvider_);
 	if (splitter_) {
@@ -117,6 +119,7 @@ QtSwift::QtSwift(po::variables_map options) : autoUpdater_(NULL) {
 				chatListWindowFactory,
 				mucSearchWindowFactory,
 				avatarStorage_,
+				capsStorage_,
 				vcardStorageFactory_,
 				application_->getApplicationMessageDisplay(),
 				options.count("latency-debug") > 0);
@@ -163,6 +166,7 @@ QtSwift::~QtSwift() {
 	foreach (QtChatListWindowFactory* factory, chatListWindowFactories_) {
 		delete factory;
 	}
+	delete capsStorage_;
 	delete avatarStorage_;
 	delete vcardStorageFactory_;
 }
diff --git a/Swift/QtUI/QtSwift.h b/Swift/QtUI/QtSwift.h
index a0ea069..4c570ae 100644
--- a/Swift/QtUI/QtSwift.h
+++ b/Swift/QtUI/QtSwift.h
@@ -27,6 +27,7 @@ namespace Swift {
 	class Application;
 	class ApplicationPathProvider;
 	class AvatarStorage;
+	class CapsStorage;
 	class MainController;
 	class QtChatWindowFactory;
 	class QtMainWindowFactory;
@@ -62,6 +63,7 @@ namespace Swift {
 			Application* application_;
 			ApplicationPathProvider* applicationPathProvider_;
 			AvatarStorage* avatarStorage_;
+			CapsStorage* capsStorage_;
 			VCardStorageFactory* vcardStorageFactory_;
 			AutoUpdater* autoUpdater_;
 
diff --git a/Swiften/Client/DummyStanzaChannel.h b/Swiften/Client/DummyStanzaChannel.h
index 05066a8..f13e587 100644
--- a/Swiften/Client/DummyStanzaChannel.h
+++ b/Swiften/Client/DummyStanzaChannel.h
@@ -48,7 +48,10 @@ namespace Swift {
 				return false;
 			}
 
-			template<typename T> bool isRequestAtIndex(int index, const JID& jid, IQ::Type type) {
+			template<typename T> bool isRequestAtIndex(size_t index, const JID& jid, IQ::Type type) {
+				if (index >= sentStanzas.size()) {
+					return false;
+				}
 				boost::shared_ptr<IQ> iqStanza = boost::dynamic_pointer_cast<IQ>(sentStanzas[index]);
 				return iqStanza && iqStanza->getType() == type && iqStanza->getTo() == jid && iqStanza->getPayload<T>();
 			}
diff --git a/Swiften/Disco/CapsFileStorage.cpp b/Swiften/Disco/CapsFileStorage.cpp
new file mode 100644
index 0000000..c5326a7
--- /dev/null
+++ b/Swiften/Disco/CapsFileStorage.cpp
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2010 Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include "Swiften/Disco/CapsFileStorage.h"
+
+#include <iostream>
+#include <boost/filesystem/fstream.hpp>
+
+#include "Swiften/Base/ByteArray.h"
+#include "Swiften/Serializer/PayloadSerializers/DiscoInfoSerializer.h"
+#include "Swiften/Parser/PayloadParsers/UnitTest/PayloadParserTester.h"
+#include "Swiften/Parser/PayloadParsers/DiscoInfoParser.h"
+#include "Swiften/StringCodecs/Hexify.h"
+#include "Swiften/StringCodecs/Base64.h"
+
+namespace Swift {
+
+CapsFileStorage::CapsFileStorage(const boost::filesystem::path& path) : path(path) {
+}
+
+DiscoInfo::ref CapsFileStorage::getDiscoInfo(const String& hash) const {
+	boost::filesystem::path capsPath(getCapsPath(hash));
+	if (boost::filesystem::exists(capsPath)) {
+		ByteArray data;
+		data.readFromFile(capsPath.string());
+
+		DiscoInfoParser parser;
+		PayloadParserTester tester(&parser);
+		tester.parse(String(data.getData(), data.getSize()));
+		return DiscoInfo::cast(parser.getPayload());
+	}
+	else {
+		return DiscoInfo::ref();
+	}
+}
+
+void CapsFileStorage::setDiscoInfo(const String& hash, DiscoInfo::ref discoInfo) {
+	boost::filesystem::path capsPath(getCapsPath(hash));
+	if (!boost::filesystem::exists(capsPath.parent_path())) {
+		try {
+			boost::filesystem::create_directories(capsPath.parent_path());
+		}
+		catch (const boost::filesystem::filesystem_error& e) {
+			std::cerr << "ERROR: " << e.what() << std::endl;
+		}
+	}
+	DiscoInfo::ref bareDiscoInfo(new DiscoInfo(*discoInfo.get()));
+	bareDiscoInfo->setNode("");
+	boost::filesystem::ofstream file(capsPath);
+	file << DiscoInfoSerializer().serializePayload(bareDiscoInfo);
+	file.close();
+}
+
+boost::filesystem::path CapsFileStorage::getCapsPath(const String& hash) const {
+	return path / (Hexify::hexify(Base64::decode(hash)) + ".xml").getUTF8String();
+}
+
+}
diff --git a/Swiften/Disco/CapsFileStorage.h b/Swiften/Disco/CapsFileStorage.h
new file mode 100644
index 0000000..ea1b1a2
--- /dev/null
+++ b/Swiften/Disco/CapsFileStorage.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2010 Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include <boost/filesystem.hpp>
+
+#include "Swiften/Disco/CapsStorage.h"
+#include "Swiften/Base/String.h"
+
+namespace Swift {
+	class CapsFileStorage : public CapsStorage {
+		public:
+			CapsFileStorage(const boost::filesystem::path& path);
+
+			virtual DiscoInfo::ref getDiscoInfo(const String& hash) const;
+			virtual void setDiscoInfo(const String& hash, DiscoInfo::ref discoInfo);
+
+		private:
+			boost::filesystem::path getCapsPath(const String& hash) const;
+
+		private:
+			boost::filesystem::path path;
+	};
+}
diff --git a/Swiften/Disco/CapsInfoGenerator.h b/Swiften/Disco/CapsInfoGenerator.h
index d69c05e..cc32bbd 100644
--- a/Swiften/Disco/CapsInfoGenerator.h
+++ b/Swiften/Disco/CapsInfoGenerator.h
@@ -4,8 +4,7 @@
  * See Documentation/Licenses/GPLv3.txt for more information.
  */
 
-#ifndef SWIFTEN_CapsInfoGenerator_H
-#define SWIFTEN_CapsInfoGenerator_H
+#pragma once
 
 #include "Swiften/Base/String.h"
 #include "Swiften/Elements/CapsInfo.h"
@@ -23,5 +22,3 @@ namespace Swift {
 			String node_;
 	};
 }
-
-#endif
diff --git a/Swiften/Disco/CapsManager.cpp b/Swiften/Disco/CapsManager.cpp
new file mode 100644
index 0000000..185f8e6
--- /dev/null
+++ b/Swiften/Disco/CapsManager.cpp
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2010 Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include "Swiften/Disco/CapsManager.h"
+
+#include <boost/bind.hpp>
+
+#include "Swiften/Client/StanzaChannel.h"
+#include "Swiften/Disco/CapsStorage.h"
+#include "Swiften/Disco/CapsInfoGenerator.h"
+#include "Swiften/Elements/CapsInfo.h"
+#include "Swiften/Queries/Requests/GetDiscoInfoRequest.h"
+
+namespace Swift {
+
+CapsManager::CapsManager(CapsStorage* capsStorage, StanzaChannel* stanzaChannel, IQRouter* iqRouter) : iqRouter(iqRouter), capsStorage(capsStorage) {
+	stanzaChannel->onPresenceReceived.connect(boost::bind(&CapsManager::handlePresenceReceived, this, _1));
+	stanzaChannel->onAvailableChanged.connect(boost::bind(&CapsManager::handleStanzaChannelAvailableChanged, this, _1));
+}
+
+void CapsManager::handlePresenceReceived(boost::shared_ptr<Presence> presence) {
+	boost::shared_ptr<CapsInfo> capsInfo = presence->getPayload<CapsInfo>();
+	if (!capsInfo || capsInfo->getHash() != "sha-1" || presence->getPayload<ErrorPayload>()) {
+		return;
+	}
+	String hash = capsInfo->getVersion();
+	if (capsStorage->getDiscoInfo(hash)) {
+		return;
+	}
+	if (failingCaps.find(std::make_pair(presence->getFrom(), hash)) != failingCaps.end()) {
+		return;
+	}
+	if (requestedDiscoInfos.find(hash) != requestedDiscoInfos.end()) {
+		fallbacks[hash].insert(std::make_pair(presence->getFrom(), capsInfo->getNode()));
+		return;
+	}
+	requestDiscoInfo(presence->getFrom(), capsInfo->getNode(), hash);
+}
+
+void CapsManager::handleStanzaChannelAvailableChanged(bool available) {
+	if (available) {
+		failingCaps.clear();
+		fallbacks.clear();
+		requestedDiscoInfos.clear();
+	}
+}
+
+void CapsManager::handleDiscoInfoReceived(const JID& from, const String& hash, DiscoInfo::ref discoInfo, const boost::optional<ErrorPayload>& error) {
+	requestedDiscoInfos.erase(hash);
+	if (error || CapsInfoGenerator("").generateCapsInfo(*discoInfo.get()).getVersion() != hash) {
+		failingCaps.insert(std::make_pair(from, hash));
+		std::map<String, std::set< std::pair<JID, String> > >::iterator i = fallbacks.find(hash);
+		if (i != fallbacks.end() && !i->second.empty()) {
+			std::pair<JID,String> fallbackAndNode = *i->second.begin();
+			i->second.erase(i->second.begin());
+			requestDiscoInfo(fallbackAndNode.first, fallbackAndNode.second, hash);
+		}
+		return;
+	}
+	fallbacks.erase(hash);
+	capsStorage->setDiscoInfo(hash, discoInfo);
+	onCapsAvailable(hash);
+}
+
+void CapsManager::requestDiscoInfo(const JID& jid, const String& node, const String& hash) {
+	boost::shared_ptr<GetDiscoInfoRequest> request(new GetDiscoInfoRequest(jid, node + "#" + hash, iqRouter));
+	request->onResponse.connect(boost::bind(&CapsManager::handleDiscoInfoReceived, this, jid, hash, _1, _2));
+	requestedDiscoInfos.insert(hash);
+	request->send();
+}
+
+DiscoInfo::ref CapsManager::getCaps(const String& hash) const {
+	return capsStorage->getDiscoInfo(hash);
+}
+
+
+}
diff --git a/Swiften/Disco/CapsManager.h b/Swiften/Disco/CapsManager.h
new file mode 100644
index 0000000..3188a07
--- /dev/null
+++ b/Swiften/Disco/CapsManager.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2010 Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include <set>
+#include <map>
+
+#include "Swiften/Base/boost_bsignals.h"
+#include "Swiften/Elements/Presence.h"
+#include "Swiften/Elements/DiscoInfo.h"
+#include "Swiften/Elements/CapsInfo.h"
+#include "Swiften/Elements/ErrorPayload.h"
+#include "Swiften/Disco/CapsProvider.h"
+
+namespace Swift {
+	class StanzaChannel;
+	class IQRouter;
+	class JID;
+	class CapsStorage;
+
+	class CapsManager : public CapsProvider, public boost::bsignals::trackable { 
+		public:
+			CapsManager(CapsStorage*, StanzaChannel*, IQRouter*);
+
+			DiscoInfo::ref getCaps(const String&) const;
+
+		private:
+			void handlePresenceReceived(boost::shared_ptr<Presence>);
+			void handleStanzaChannelAvailableChanged(bool);
+			void handleDiscoInfoReceived(const JID&, const String& hash, DiscoInfo::ref, const boost::optional<ErrorPayload>&);
+			void requestDiscoInfo(const JID& jid, const String& node, const String& hash);
+
+		private:
+			IQRouter* iqRouter;
+			CapsStorage* capsStorage;
+			std::set<String> requestedDiscoInfos;
+			std::set< std::pair<JID, String> > failingCaps;
+			std::map<String, std::set< std::pair<JID, String> > > fallbacks;
+	};
+}
diff --git a/Swiften/Disco/CapsMemoryStorage.h b/Swiften/Disco/CapsMemoryStorage.h
new file mode 100644
index 0000000..71bd5d5
--- /dev/null
+++ b/Swiften/Disco/CapsMemoryStorage.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2010 Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include <boost/shared_ptr.hpp>
+#include <map>
+
+#include "Swiften/Base/String.h"
+#include "Swiften/Disco/CapsStorage.h"
+
+namespace Swift {
+	class CapsMemoryStorage : public CapsStorage {
+		public:
+			CapsMemoryStorage() {}
+
+			virtual DiscoInfo::ref getDiscoInfo(const String& hash) const {
+				CapsMap::const_iterator i = caps.find(hash);
+				if (i != caps.end()) {
+					return i->second;
+				}
+				else {
+					return DiscoInfo::ref();
+				}
+			}
+
+			virtual void setDiscoInfo(const String& hash, DiscoInfo::ref discoInfo) {
+				caps[hash] = discoInfo;
+			}
+
+		private:
+			typedef std::map<String, DiscoInfo::ref> CapsMap;
+			CapsMap caps;
+	};
+}
diff --git a/Swiften/Disco/CapsProvider.h b/Swiften/Disco/CapsProvider.h
new file mode 100644
index 0000000..2aaad79
--- /dev/null
+++ b/Swiften/Disco/CapsProvider.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2010 Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include "Swiften/Base/boost_bsignals.h"
+#include "Swiften/Elements/DiscoInfo.h"
+#include "Swiften/Elements/CapsInfo.h"
+
+namespace Swift {
+	class String;
+
+	class CapsProvider { 
+		public:
+			virtual ~CapsProvider() {}
+
+			virtual DiscoInfo::ref getCaps(const String&) const = 0;
+
+			boost::signal<void (const String&)> onCapsAvailable;
+	};
+}
diff --git a/Swiften/Disco/CapsStorage.cpp b/Swiften/Disco/CapsStorage.cpp
new file mode 100644
index 0000000..acb58fe
--- /dev/null
+++ b/Swiften/Disco/CapsStorage.cpp
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2010 Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include "Swiften/Disco/CapsStorage.h"
+
+namespace Swift {
+
+CapsStorage::~CapsStorage() {
+}
+
+}
diff --git a/Swiften/Disco/CapsStorage.h b/Swiften/Disco/CapsStorage.h
new file mode 100644
index 0000000..e4ff945
--- /dev/null
+++ b/Swiften/Disco/CapsStorage.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2010 Remko Tronçon
+ * 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/Elements/DiscoInfo.h"
+
+namespace Swift {
+	class String;
+
+	class CapsStorage {
+		public:
+			virtual ~CapsStorage();
+
+			virtual DiscoInfo::ref getDiscoInfo(const String&) const = 0;
+			virtual void setDiscoInfo(const String&, DiscoInfo::ref) = 0;
+	};
+}
diff --git a/Swiften/Disco/EntityCapsManager.cpp b/Swiften/Disco/EntityCapsManager.cpp
new file mode 100644
index 0000000..26e0594
--- /dev/null
+++ b/Swiften/Disco/EntityCapsManager.cpp
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2010 Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include "Swiften/Disco/EntityCapsManager.h"
+
+#include <boost/bind.hpp>
+
+#include "Swiften/Disco/CapsProvider.h"
+#include "Swiften/Client/StanzaChannel.h"
+
+namespace Swift {
+
+EntityCapsManager::EntityCapsManager(CapsProvider* capsProvider, StanzaChannel* stanzaChannel) : capsProvider(capsProvider) {
+	stanzaChannel->onPresenceReceived.connect(boost::bind(&EntityCapsManager::handlePresenceReceived, this, _1));
+	stanzaChannel->onAvailableChanged.connect(boost::bind(&EntityCapsManager::handleStanzaChannelAvailableChanged, this, _1));
+	capsProvider->onCapsAvailable.connect(boost::bind(&EntityCapsManager::handleCapsAvailable, this, _1));
+}
+
+void EntityCapsManager::handlePresenceReceived(boost::shared_ptr<Presence> presence) {
+	JID from = presence->getFrom();
+	if (presence->isAvailable()) {
+		boost::shared_ptr<CapsInfo> capsInfo = presence->getPayload<CapsInfo>();
+		if (!capsInfo || capsInfo->getHash() != "sha-1" || presence->getPayload<ErrorPayload>()) {
+			return;
+		}
+		String hash = capsInfo->getVersion();
+		std::map<JID, String>::iterator i = caps.find(from);
+		if (i == caps.end() || i->second != hash) {
+			caps.insert(std::make_pair(from, hash));
+			DiscoInfo::ref disco = capsProvider->getCaps(hash);
+			if (disco) {
+				onCapsChanged(from);
+			}
+			else if (i != caps.end()) {
+				caps.erase(i);
+				onCapsChanged(from);
+			}
+		}
+	}
+	else {
+		std::map<JID, String>::iterator i = caps.find(from);
+		if (i != caps.end()) {
+			caps.erase(i);
+			onCapsChanged(from);
+		}
+	}
+}
+
+void EntityCapsManager::handleStanzaChannelAvailableChanged(bool available) {
+	if (available) {
+		std::map<JID, String> capsCopy;
+		capsCopy.swap(caps);
+		for (std::map<JID,String>::const_iterator i = capsCopy.begin(); i != capsCopy.end(); ++i) {
+			onCapsChanged(i->first);
+		}
+	}
+}
+
+void EntityCapsManager::handleCapsAvailable(const String& hash) {
+	// TODO: Use Boost.Bimap ?
+	for (std::map<JID,String>::const_iterator i = caps.begin(); i != caps.end(); ++i) {
+		if (i->second == hash) {
+			onCapsChanged(i->first);
+		}
+	}
+}
+
+DiscoInfo::ref EntityCapsManager::getCaps(const JID& jid) const {
+	std::map<JID, String>::const_iterator i = caps.find(jid);
+	if (i != caps.end()) {
+		return capsProvider->getCaps(i->second);
+	}
+	return DiscoInfo::ref();
+}
+
+}
diff --git a/Swiften/Disco/EntityCapsManager.h b/Swiften/Disco/EntityCapsManager.h
new file mode 100644
index 0000000..810e260
--- /dev/null
+++ b/Swiften/Disco/EntityCapsManager.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2010 Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include <map>
+
+#include "Swiften/Base/boost_bsignals.h"
+#include "Swiften/Elements/Presence.h"
+#include "Swiften/Elements/DiscoInfo.h"
+#include "Swiften/Elements/ErrorPayload.h"
+
+namespace Swift {
+	class StanzaChannel;
+	class CapsProvider;
+
+	class EntityCapsManager : public boost::bsignals::trackable { 
+		public:
+			EntityCapsManager(CapsProvider*, StanzaChannel*);
+
+			DiscoInfo::ref getCaps(const JID&) const;
+
+			boost::signal<void (const JID&)> onCapsChanged;
+
+		private:
+			void handlePresenceReceived(boost::shared_ptr<Presence>);
+			void handleStanzaChannelAvailableChanged(bool);
+			void handleCapsAvailable(const String&);
+
+		private:
+			CapsProvider* capsProvider;
+			std::map<JID, String> caps;
+	};
+}
diff --git a/Swiften/Disco/SConscript b/Swiften/Disco/SConscript
new file mode 100644
index 0000000..9da3fb3
--- /dev/null
+++ b/Swiften/Disco/SConscript
@@ -0,0 +1,10 @@
+Import("swiften_env")
+
+objects = swiften_env.StaticObject([
+			"CapsInfoGenerator.cpp",
+			"CapsManager.cpp",
+			"EntityCapsManager.cpp",
+			"CapsStorage.cpp",
+			"CapsFileStorage.cpp",
+		])
+swiften_env.Append(SWIFTEN_OBJECTS = [objects])
diff --git a/Swiften/Disco/UnitTest/CapsManagerTest.cpp b/Swiften/Disco/UnitTest/CapsManagerTest.cpp
new file mode 100644
index 0000000..2156c3e
--- /dev/null
+++ b/Swiften/Disco/UnitTest/CapsManagerTest.cpp
@@ -0,0 +1,271 @@
+/*
+ * Copyright (c) 2010 Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include <cppunit/extensions/HelperMacros.h>
+#include <cppunit/extensions/TestFactoryRegistry.h>
+#include <vector>
+#include <boost/bind.hpp>
+
+#include "Swiften/Disco/CapsManager.h"
+#include "Swiften/Disco/CapsMemoryStorage.h"
+#include "Swiften/Disco/CapsInfoGenerator.h"
+#include "Swiften/Queries/IQRouter.h"
+#include "Swiften/Elements/CapsInfo.h"
+#include "Swiften/Elements/DiscoInfo.h"
+#include "Swiften/Client/DummyStanzaChannel.h"
+
+using namespace Swift;
+
+class CapsManagerTest : public CppUnit::TestFixture {
+		CPPUNIT_TEST_SUITE(CapsManagerTest);
+		CPPUNIT_TEST(testReceiveNewHashRequestsDisco);
+		CPPUNIT_TEST(testReceiveSameHashDoesNotRequestDisco);
+		CPPUNIT_TEST(testReceiveLegacyCapsDoesNotRequestDisco);
+		CPPUNIT_TEST(testReceiveSameHashFromSameUserAfterFailedDiscoDoesNotRequestDisco);
+		CPPUNIT_TEST(testReceiveSameHashFromDifferentUserAfterFailedDiscoRequestsDisco);
+		CPPUNIT_TEST(testReceiveSameHashFromDifferentUserAfterIncorrectVerificationRequestsDisco);
+		CPPUNIT_TEST(testReceiveDifferentHashFromSameUserAfterFailedDiscoDoesNotRequestDisco);
+		CPPUNIT_TEST(testReceiveSameHashAfterSuccesfulDiscoDoesNotRequestDisco);
+		CPPUNIT_TEST(testReceiveSuccesfulDiscoStoresCaps);
+		CPPUNIT_TEST(testReceiveIncorrectVerificationDiscoDoesNotStoreCaps);
+		CPPUNIT_TEST(testReceiveFailingDiscoFallsBack);
+		CPPUNIT_TEST(testReceiveFailingFallbackDiscoFallsBack);
+		CPPUNIT_TEST(testReceiveSameHashFromFailingUserAfterReconnectRequestsDisco);
+		CPPUNIT_TEST(testReconnectResetsFallback);
+		CPPUNIT_TEST(testReconnectResetsRequests);
+		CPPUNIT_TEST_SUITE_END();
+
+	public:
+		void setUp() {
+			stanzaChannel = new DummyStanzaChannel();
+			iqRouter = new IQRouter(stanzaChannel);
+			storage = new CapsMemoryStorage();
+			user1 = JID("user1@bar.com/bla");
+			discoInfo1 = boost::shared_ptr<DiscoInfo>(new DiscoInfo());
+			discoInfo1->addFeature("http://swift.im/feature1");
+			capsInfo1 = boost::shared_ptr<CapsInfo>(new CapsInfo(CapsInfoGenerator("http://node1.im").generateCapsInfo(*discoInfo1.get())));
+			capsInfo1alt = boost::shared_ptr<CapsInfo>(new CapsInfo(CapsInfoGenerator("http://node2.im").generateCapsInfo(*discoInfo1.get())));
+			user2 = JID("user2@foo.com/baz");
+			discoInfo2 = boost::shared_ptr<DiscoInfo>(new DiscoInfo());
+			discoInfo2->addFeature("http://swift.im/feature2");
+			capsInfo2 = boost::shared_ptr<CapsInfo>(new CapsInfo(CapsInfoGenerator("http://node2.im").generateCapsInfo(*discoInfo2.get())));
+			user3 = JID("user3@foo.com/baz");
+			legacyCapsInfo = boost::shared_ptr<CapsInfo>(new CapsInfo("http://swift.im", "ver1", ""));
+		}
+
+		void tearDown() {
+			delete iqRouter;
+			delete stanzaChannel;
+		}
+
+		void testReceiveNewHashRequestsDisco() {
+			std::auto_ptr<CapsManager> testling = createManager();
+			sendPresenceWithCaps(user1, capsInfo1);
+
+			CPPUNIT_ASSERT(stanzaChannel->isRequestAtIndex<DiscoInfo>(0, user1, IQ::Get));
+			boost::shared_ptr<DiscoInfo> discoInfo(stanzaChannel->sentStanzas[0]->getPayload<DiscoInfo>());
+			CPPUNIT_ASSERT(discoInfo);
+			CPPUNIT_ASSERT_EQUAL("http://node1.im#" + capsInfo1->getVersion(), discoInfo->getNode());
+		}
+
+		void testReceiveSameHashDoesNotRequestDisco() {
+			std::auto_ptr<CapsManager> testling = createManager();
+			sendPresenceWithCaps(user1, capsInfo1);
+			stanzaChannel->sentStanzas.clear();
+			sendPresenceWithCaps(user1, capsInfo1);
+
+			CPPUNIT_ASSERT_EQUAL(0, static_cast<int>(stanzaChannel->sentStanzas.size()));
+		}
+
+		void testReceiveLegacyCapsDoesNotRequestDisco() {
+			std::auto_ptr<CapsManager> testling = createManager();
+			sendPresenceWithCaps(user1, legacyCapsInfo);
+
+			CPPUNIT_ASSERT_EQUAL(0, static_cast<int>(stanzaChannel->sentStanzas.size()));
+		}
+
+		void testReceiveSameHashAfterSuccesfulDiscoDoesNotRequestDisco() {
+			std::auto_ptr<CapsManager> testling = createManager();
+			sendPresenceWithCaps(user1, capsInfo1);
+			sendDiscoInfoResult(discoInfo1);
+
+			stanzaChannel->sentStanzas.clear();
+			sendPresenceWithCaps(user1, capsInfo1);
+
+			CPPUNIT_ASSERT_EQUAL(0, static_cast<int>(stanzaChannel->sentStanzas.size()));
+		}
+
+		void testReceiveSameHashFromSameUserAfterFailedDiscoDoesNotRequestDisco() {
+			std::auto_ptr<CapsManager> testling = createManager();
+			sendPresenceWithCaps(user1, capsInfo1);
+			stanzaChannel->onIQReceived(IQ::createError(JID("baz@fum.com/foo"), stanzaChannel->sentStanzas[0]->getID()));
+
+			stanzaChannel->sentStanzas.clear();
+			sendPresenceWithCaps(user1, capsInfo1);
+
+			CPPUNIT_ASSERT_EQUAL(0, static_cast<int>(stanzaChannel->sentStanzas.size()));
+		}
+
+		void testReceiveSameHashFromSameUserAfterIncorrectVerificationDoesNotRequestDisco() {
+			std::auto_ptr<CapsManager> testling = createManager();
+			sendPresenceWithCaps(user1, capsInfo1);
+			sendDiscoInfoResult(discoInfo2);
+
+			stanzaChannel->sentStanzas.clear();
+			sendPresenceWithCaps(user1, capsInfo1);
+
+			CPPUNIT_ASSERT_EQUAL(0, static_cast<int>(stanzaChannel->sentStanzas.size()));
+		}
+
+		void testReceiveSameHashFromDifferentUserAfterFailedDiscoRequestsDisco() {
+			std::auto_ptr<CapsManager> testling = createManager();
+			sendPresenceWithCaps(user1, capsInfo1);
+			stanzaChannel->onIQReceived(IQ::createError(JID("baz@fum.com/foo"), stanzaChannel->sentStanzas[0]->getID()));
+
+			stanzaChannel->sentStanzas.clear();
+			sendPresenceWithCaps(user2, capsInfo1);
+			CPPUNIT_ASSERT(stanzaChannel->isRequestAtIndex<DiscoInfo>(0, user2, IQ::Get));
+		}
+
+		void testReceiveSameHashFromDifferentUserAfterIncorrectVerificationRequestsDisco() {
+			std::auto_ptr<CapsManager> testling = createManager();
+			sendPresenceWithCaps(user1, capsInfo1);
+			sendDiscoInfoResult(discoInfo2);
+
+			stanzaChannel->sentStanzas.clear();
+			sendPresenceWithCaps(user2, capsInfo1);
+			CPPUNIT_ASSERT(stanzaChannel->isRequestAtIndex<DiscoInfo>(0, user2, IQ::Get));
+		}
+
+		void testReceiveDifferentHashFromSameUserAfterFailedDiscoDoesNotRequestDisco() {
+			std::auto_ptr<CapsManager> testling = createManager();
+			sendPresenceWithCaps(user1, capsInfo1);
+			stanzaChannel->onIQReceived(IQ::createError(JID("baz@fum.com/foo"), stanzaChannel->sentStanzas[0]->getID()));
+
+			stanzaChannel->sentStanzas.clear();
+			sendPresenceWithCaps(user1, capsInfo2);
+
+			CPPUNIT_ASSERT(stanzaChannel->isRequestAtIndex<DiscoInfo>(0, user1, IQ::Get));
+		}
+
+		void testReceiveSuccesfulDiscoStoresCaps() {
+			std::auto_ptr<CapsManager> testling = createManager();
+			sendPresenceWithCaps(user1, capsInfo1);
+			sendDiscoInfoResult(discoInfo1);
+
+			boost::shared_ptr<DiscoInfo> discoInfo(storage->getDiscoInfo(capsInfo1->getVersion()));
+			CPPUNIT_ASSERT(discoInfo);
+			CPPUNIT_ASSERT(discoInfo->hasFeature("http://swift.im/feature1"));
+		}
+
+		void testReceiveIncorrectVerificationDiscoDoesNotStoreCaps() {
+			std::auto_ptr<CapsManager> testling = createManager();
+			sendPresenceWithCaps(user1, capsInfo1);
+			sendDiscoInfoResult(discoInfo2);
+
+			boost::shared_ptr<DiscoInfo> discoInfo(storage->getDiscoInfo(capsInfo1->getVersion()));
+			CPPUNIT_ASSERT(!discoInfo);
+		}
+
+		void testReceiveFailingDiscoFallsBack() {
+			std::auto_ptr<CapsManager> testling = createManager();
+			sendPresenceWithCaps(user1, capsInfo1);
+			sendPresenceWithCaps(user2, capsInfo1alt);
+			stanzaChannel->onIQReceived(IQ::createError(JID("baz@fum.com/foo"), stanzaChannel->sentStanzas[0]->getID()));
+
+			CPPUNIT_ASSERT(stanzaChannel->isRequestAtIndex<DiscoInfo>(1, user2, IQ::Get));
+			boost::shared_ptr<DiscoInfo> discoInfo(stanzaChannel->sentStanzas[1]->getPayload<DiscoInfo>());
+			CPPUNIT_ASSERT(discoInfo);
+			CPPUNIT_ASSERT_EQUAL("http://node2.im#" + capsInfo1alt->getVersion(), discoInfo->getNode());
+		}
+
+		void testReceiveFailingFallbackDiscoFallsBack() {
+			std::auto_ptr<CapsManager> testling = createManager();
+			sendPresenceWithCaps(user1, capsInfo1);
+			sendPresenceWithCaps(user2, capsInfo1alt);
+			sendPresenceWithCaps(user3, capsInfo1);
+			stanzaChannel->onIQReceived(IQ::createError(JID("baz@fum.com/foo"), stanzaChannel->sentStanzas[0]->getID()));
+			stanzaChannel->onIQReceived(IQ::createError(JID("baz@fum.com/foo"), stanzaChannel->sentStanzas[1]->getID()));
+
+			CPPUNIT_ASSERT(stanzaChannel->isRequestAtIndex<DiscoInfo>(2, user3, IQ::Get));
+		}
+
+		void testReceiveSameHashFromFailingUserAfterReconnectRequestsDisco() {
+			std::auto_ptr<CapsManager> testling = createManager();
+			sendPresenceWithCaps(user1, capsInfo1);
+			stanzaChannel->onIQReceived(IQ::createError(JID("baz@fum.com/foo"), stanzaChannel->sentStanzas[0]->getID()));
+			stanzaChannel->setAvailable(false);
+			stanzaChannel->setAvailable(true);
+			stanzaChannel->sentStanzas.clear();
+
+			sendPresenceWithCaps(user1, capsInfo1);
+
+			CPPUNIT_ASSERT(stanzaChannel->isRequestAtIndex<DiscoInfo>(0, user1, IQ::Get));
+		}
+
+		void testReconnectResetsFallback() {
+			std::auto_ptr<CapsManager> testling = createManager();
+			sendPresenceWithCaps(user1, capsInfo1);
+			sendPresenceWithCaps(user2, capsInfo1alt);
+			stanzaChannel->setAvailable(false);
+			stanzaChannel->setAvailable(true);
+			stanzaChannel->sentStanzas.clear();
+			sendPresenceWithCaps(user1, capsInfo1);
+			stanzaChannel->onIQReceived(IQ::createError(JID("baz@fum.com/foo"), stanzaChannel->sentStanzas[0]->getID()));
+
+			CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(stanzaChannel->sentStanzas.size()));
+		}
+
+		void testReconnectResetsRequests() {
+			std::auto_ptr<CapsManager> testling = createManager();
+			sendPresenceWithCaps(user1, capsInfo1);
+			stanzaChannel->sentStanzas.clear();
+			stanzaChannel->setAvailable(false);
+			stanzaChannel->setAvailable(true);
+			sendPresenceWithCaps(user1, capsInfo1);
+
+			CPPUNIT_ASSERT(stanzaChannel->isRequestAtIndex<DiscoInfo>(0, user1, IQ::Get));
+		}
+
+	private:
+		std::auto_ptr<CapsManager> createManager() {
+			std::auto_ptr<CapsManager> manager(new CapsManager(storage, stanzaChannel, iqRouter));
+			//manager->onCapsChanged.connect(boost::bind(&CapsManagerTest::handleCapsChanged, this, _1));
+			return manager;
+		}
+
+		void handleCapsChanged(const JID& jid) {
+			changes.push_back(jid);
+		}
+
+		void sendPresenceWithCaps(const JID& jid, boost::shared_ptr<CapsInfo> caps) {
+			boost::shared_ptr<Presence> presence(new Presence());
+			presence->setFrom(jid);
+			presence->addPayload(caps);
+			stanzaChannel->onPresenceReceived(presence);
+		}
+
+		void sendDiscoInfoResult(boost::shared_ptr<DiscoInfo> discoInfo) {
+			stanzaChannel->onIQReceived(IQ::createResult(JID("baz@fum.com/dum"), stanzaChannel->sentStanzas[0]->getID(), discoInfo));
+		}
+
+	private:
+		DummyStanzaChannel* stanzaChannel;
+		IQRouter* iqRouter;
+		CapsStorage* storage;
+		std::vector<JID> changes;
+		JID user1;
+		boost::shared_ptr<DiscoInfo> discoInfo1;
+		boost::shared_ptr<CapsInfo> capsInfo1;
+		boost::shared_ptr<CapsInfo> capsInfo1alt;
+		JID user2;
+		boost::shared_ptr<DiscoInfo> discoInfo2;
+		boost::shared_ptr<CapsInfo> capsInfo2;
+		boost::shared_ptr<CapsInfo> legacyCapsInfo;
+		JID user3;
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(CapsManagerTest);
diff --git a/Swiften/Disco/UnitTest/EntityCapsManagerTest.cpp b/Swiften/Disco/UnitTest/EntityCapsManagerTest.cpp
new file mode 100644
index 0000000..0a498cf
--- /dev/null
+++ b/Swiften/Disco/UnitTest/EntityCapsManagerTest.cpp
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2010 Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include <cppunit/extensions/HelperMacros.h>
+#include <cppunit/extensions/TestFactoryRegistry.h>
+#include <vector>
+#include <boost/bind.hpp>
+
+#include "Swiften/Disco/EntityCapsManager.h"
+#include "Swiften/Disco/CapsProvider.h"
+#include "Swiften/Elements/CapsInfo.h"
+#include "Swiften/Client/DummyStanzaChannel.h"
+#include "Swiften/Disco/CapsInfoGenerator.h"
+
+using namespace Swift;
+
+class EntityCapsManagerTest : public CppUnit::TestFixture {
+		CPPUNIT_TEST_SUITE(EntityCapsManagerTest);
+		CPPUNIT_TEST(testReceiveKnownHash);
+		CPPUNIT_TEST(testReceiveKnownHashTwiceDoesNotTriggerChange);
+		CPPUNIT_TEST(testReceiveUnknownHashDoesNotTriggerChange);
+		CPPUNIT_TEST(testReceiveUnknownHashAfterKnownHashTriggersChangeAndClearsCaps);
+		CPPUNIT_TEST(testReceiveUnavailablePresenceAfterKnownHashTriggersChangeAndClearsCaps);
+		CPPUNIT_TEST(testReconnectTriggersChangeAndClearsCaps);
+		CPPUNIT_TEST(testHashAvailable);
+		CPPUNIT_TEST_SUITE_END();
+
+	public:
+		void setUp() {
+			stanzaChannel = new DummyStanzaChannel();
+			capsProvider = new DummyCapsProvider();
+
+			user1 = JID("user1@bar.com/bla");
+			discoInfo1 = boost::shared_ptr<DiscoInfo>(new DiscoInfo());
+			discoInfo1->addFeature("http://swift.im/feature1");
+			capsInfo1 = boost::shared_ptr<CapsInfo>(new CapsInfo(CapsInfoGenerator("http://node1.im").generateCapsInfo(*discoInfo1.get())));
+			capsInfo1alt = boost::shared_ptr<CapsInfo>(new CapsInfo(CapsInfoGenerator("http://node2.im").generateCapsInfo(*discoInfo1.get())));
+			user2 = JID("user2@foo.com/baz");
+			discoInfo2 = boost::shared_ptr<DiscoInfo>(new DiscoInfo());
+			discoInfo2->addFeature("http://swift.im/feature2");
+			capsInfo2 = boost::shared_ptr<CapsInfo>(new CapsInfo(CapsInfoGenerator("http://node2.im").generateCapsInfo(*discoInfo2.get())));
+			user3 = JID("user3@foo.com/baz");
+			legacyCapsInfo = boost::shared_ptr<CapsInfo>(new CapsInfo("http://swift.im", "ver1", ""));
+		}
+
+		void tearDown() {
+			delete capsProvider;
+			delete stanzaChannel;
+		}
+
+		void testReceiveKnownHash() {
+			std::auto_ptr<EntityCapsManager> testling = createManager();
+			capsProvider->caps[capsInfo1->getVersion()] = discoInfo1;
+			sendPresenceWithCaps(user1, capsInfo1);
+
+			CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(changes.size()));
+			CPPUNIT_ASSERT_EQUAL(user1, changes[0]);
+			CPPUNIT_ASSERT_EQUAL(discoInfo1, testling->getCaps(user1));
+		}
+
+		void testReceiveKnownHashTwiceDoesNotTriggerChange() {
+			std::auto_ptr<EntityCapsManager> testling = createManager();
+			capsProvider->caps[capsInfo1->getVersion()] = discoInfo1;
+			sendPresenceWithCaps(user1, capsInfo1);
+			changes.clear();
+
+			sendPresenceWithCaps(user1, capsInfo1);
+
+			CPPUNIT_ASSERT_EQUAL(0, static_cast<int>(changes.size()));
+		}
+
+		void testReceiveUnknownHashDoesNotTriggerChange() {
+			std::auto_ptr<EntityCapsManager> testling = createManager();
+			sendPresenceWithCaps(user1, capsInfo1);
+
+			CPPUNIT_ASSERT_EQUAL(0, static_cast<int>(changes.size()));
+		}
+
+		void testHashAvailable() {
+			std::auto_ptr<EntityCapsManager> testling = createManager();
+			sendPresenceWithCaps(user1, capsInfo1);
+
+			capsProvider->caps[capsInfo1->getVersion()] = discoInfo1;
+			capsProvider->onCapsAvailable(capsInfo1->getVersion());
+
+			CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(changes.size()));
+			CPPUNIT_ASSERT_EQUAL(user1, changes[0]);
+			CPPUNIT_ASSERT_EQUAL(discoInfo1, testling->getCaps(user1));
+		}
+
+		void testReceiveUnknownHashAfterKnownHashTriggersChangeAndClearsCaps() {
+			std::auto_ptr<EntityCapsManager> testling = createManager();
+			capsProvider->caps[capsInfo1->getVersion()] = discoInfo1;
+			sendPresenceWithCaps(user1, capsInfo1);
+			changes.clear();
+			sendPresenceWithCaps(user1, capsInfo2);
+
+			CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(changes.size()));
+			CPPUNIT_ASSERT_EQUAL(user1, changes[0]);
+			CPPUNIT_ASSERT(!testling->getCaps(user1));
+		}
+
+		void testReceiveUnavailablePresenceAfterKnownHashTriggersChangeAndClearsCaps() {
+			std::auto_ptr<EntityCapsManager> testling = createManager();
+			capsProvider->caps[capsInfo1->getVersion()] = discoInfo1;
+			sendPresenceWithCaps(user1, capsInfo1);
+			changes.clear();
+			sendUnavailablePresence(user1);
+
+			CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(changes.size()));
+			CPPUNIT_ASSERT_EQUAL(user1, changes[0]);
+			CPPUNIT_ASSERT(!testling->getCaps(user1));
+		}
+
+		void testReconnectTriggersChangeAndClearsCaps() {
+			std::auto_ptr<EntityCapsManager> testling = createManager();
+			capsProvider->caps[capsInfo1->getVersion()] = discoInfo1;
+			capsProvider->caps[capsInfo2->getVersion()] = discoInfo2;
+			sendPresenceWithCaps(user1, capsInfo1);
+			sendPresenceWithCaps(user2, capsInfo2);
+			changes.clear();
+			stanzaChannel->setAvailable(false);
+			stanzaChannel->setAvailable(true);
+
+			CPPUNIT_ASSERT_EQUAL(2, static_cast<int>(changes.size()));
+			CPPUNIT_ASSERT_EQUAL(user1, changes[0]);
+			CPPUNIT_ASSERT(!testling->getCaps(user1));
+			CPPUNIT_ASSERT_EQUAL(user2, changes[1]);
+			CPPUNIT_ASSERT(!testling->getCaps(user2));
+		}
+
+	private:
+		std::auto_ptr<EntityCapsManager> createManager() {
+			std::auto_ptr<EntityCapsManager> manager(new EntityCapsManager(capsProvider, stanzaChannel));
+			manager->onCapsChanged.connect(boost::bind(&EntityCapsManagerTest::handleCapsChanged, this, _1));
+			return manager;
+		}
+
+		void handleCapsChanged(const JID& jid) {
+			changes.push_back(jid);
+		}
+
+		void sendPresenceWithCaps(const JID& jid, boost::shared_ptr<CapsInfo> caps) {
+			boost::shared_ptr<Presence> presence(new Presence());
+			presence->setFrom(jid);
+			presence->addPayload(caps);
+			stanzaChannel->onPresenceReceived(presence);
+		}
+
+		void sendUnavailablePresence(const JID& jid) {
+			boost::shared_ptr<Presence> presence(new Presence());
+			presence->setFrom(jid);
+			presence->setType(Presence::Unavailable);
+			stanzaChannel->onPresenceReceived(presence);
+		}
+	
+	private:
+		struct DummyCapsProvider : public CapsProvider {
+			virtual DiscoInfo::ref getCaps(const String& hash) const {
+				std::map<String, DiscoInfo::ref>::const_iterator i = caps.find(hash);
+				if (i != caps.end()) {
+					return i->second;
+				}
+				return DiscoInfo::ref();
+			}
+
+			std::map<String, DiscoInfo::ref> caps;
+		};
+
+	private:
+		DummyStanzaChannel* stanzaChannel;
+		DummyCapsProvider* capsProvider;
+		JID user1;
+		boost::shared_ptr<DiscoInfo> discoInfo1;
+		boost::shared_ptr<CapsInfo> capsInfo1;
+		boost::shared_ptr<CapsInfo> capsInfo1alt;
+		JID user2;
+		boost::shared_ptr<DiscoInfo> discoInfo2;
+		boost::shared_ptr<CapsInfo> capsInfo2;
+		boost::shared_ptr<CapsInfo> legacyCapsInfo;
+		JID user3;
+		std::vector<JID> changes;
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION(EntityCapsManagerTest);
diff --git a/Swiften/Elements/CapsInfo.h b/Swiften/Elements/CapsInfo.h
index 6fde13b..0fce90c 100644
--- a/Swiften/Elements/CapsInfo.h
+++ b/Swiften/Elements/CapsInfo.h
@@ -4,20 +4,49 @@
  * See Documentation/Licenses/GPLv3.txt for more information.
  */
 
-#ifndef SWIFTEN_CapsInfo_H
-#define SWIFTEN_CapsInfo_H
+#pragma once
 
 #include "Swiften/Base/String.h"
+#include "Swiften/Base/Shared.h"
 #include "Swiften/Elements/Payload.h"
 
 namespace Swift {
-	class CapsInfo : public Payload {
+	class CapsInfo : public Payload, public Shared<CapsInfo> {
 		public:
-			CapsInfo(const String& node, const String& version, const String& hash = "sha-1") : node_(node), version_(version), hash_(hash) {}
+			CapsInfo(const String& node = "", const String& version = "", const String& hash = "sha-1") : node_(node), version_(version), hash_(hash) {}
+
+			bool operator==(const CapsInfo& o) const {
+				return o.node_ == node_ && o.version_ == version_ && o.hash_ == hash_;
+			}
+
+			bool operator<(const CapsInfo& o) const {
+				if (o.node_ == node_) {
+					if (o.version_ == version_) {
+						return hash_ < o.hash_;
+					}
+					else {
+						return version_ < o.version_;
+					}
+				}
+				else {
+					return node_ < o.node_;
+				}
+			}
 
 			const String& getNode() const { return node_; }
+			void setNode(const String& node) {
+				node_ = node;
+			}
+
 			const String& getVersion() const { return version_; }
+			void setVersion(const String& version) {
+				version_ = version;
+			}
+
 			const String& getHash() const { return hash_; }
+			void setHash(const String& hash) {
+				hash_ = hash;
+			}
 
 		private:
 			String node_;
@@ -25,5 +54,3 @@ namespace Swift {
 			String hash_;
 	};
 }
-
-#endif
diff --git a/Swiften/Elements/DiscoInfo.h b/Swiften/Elements/DiscoInfo.h
index af4a5dc..cee9200 100644
--- a/Swiften/Elements/DiscoInfo.h
+++ b/Swiften/Elements/DiscoInfo.h
@@ -12,9 +12,10 @@
 
 #include "Swiften/Elements/Payload.h"
 #include "Swiften/Base/String.h"
+#include "Swiften/Base/Shared.h"
 
 namespace Swift {
-	class DiscoInfo : public Payload {
+	class DiscoInfo : public Payload, public Shared<DiscoInfo> {
 		public:
 			const static std::string SecurityLabels;
 			class Identity {
diff --git a/Swiften/Elements/Presence.h b/Swiften/Elements/Presence.h
index 0789b18..7297339 100644
--- a/Swiften/Elements/Presence.h
+++ b/Swiften/Elements/Presence.h
@@ -62,6 +62,10 @@ namespace Swift {
 				return boost::shared_ptr<Presence>(new Presence(*this));
 			}
 
+			bool isAvailable() const {
+				return type_ == Available;
+			}
+
 		private:
 			Presence::Type type_;
 	};
diff --git a/Swiften/Parser/PayloadParsers/CapsInfoParser.cpp b/Swiften/Parser/PayloadParsers/CapsInfoParser.cpp
new file mode 100644
index 0000000..49c271c
--- /dev/null
+++ b/Swiften/Parser/PayloadParsers/CapsInfoParser.cpp
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2010 Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#include "Swiften/Parser/PayloadParsers/CapsInfoParser.h"
+
+#include <locale>
+
+#include <boost/date_time/time_facet.hpp>
+
+namespace Swift {
+
+CapsInfoParser::CapsInfoParser() : level(0) {
+}
+
+void CapsInfoParser::handleStartElement(const String&, const String& /*ns*/, const AttributeMap& attributes) {
+	if (level == 0) {
+		getPayloadInternal()->setHash(attributes.getAttribute("hash"));
+		getPayloadInternal()->setNode(attributes.getAttribute("node"));
+		getPayloadInternal()->setVersion(attributes.getAttribute("ver"));
+	}
+	++level;
+}
+
+void CapsInfoParser::handleEndElement(const String&, const String&) {
+	--level;
+}
+
+void CapsInfoParser::handleCharacterData(const String&) {
+
+}
+
+}
diff --git a/Swiften/Parser/PayloadParsers/CapsInfoParser.h b/Swiften/Parser/PayloadParsers/CapsInfoParser.h
new file mode 100644
index 0000000..6058837
--- /dev/null
+++ b/Swiften/Parser/PayloadParsers/CapsInfoParser.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2010 Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include "Swiften/Elements/CapsInfo.h"
+#include "Swiften/Parser/GenericPayloadParser.h"
+
+namespace Swift {
+	class CapsInfoParser : public GenericPayloadParser<CapsInfo> {
+		public:
+			CapsInfoParser();
+
+			virtual void handleStartElement(const String& element, const String&, const AttributeMap& attributes);
+			virtual void handleEndElement(const String& element, const String&);
+			virtual void handleCharacterData(const String& data);
+
+		private:
+			int level;
+	};
+}
diff --git a/Swiften/Parser/PayloadParsers/CapsInfoParserFactory.h b/Swiften/Parser/PayloadParsers/CapsInfoParserFactory.h
new file mode 100644
index 0000000..6910ff8
--- /dev/null
+++ b/Swiften/Parser/PayloadParsers/CapsInfoParserFactory.h
@@ -0,0 +1,17 @@
+/*
+ * Copyright (c) 2010 Remko Tronçon
+ * Licensed under the GNU General Public License v3.
+ * See Documentation/Licenses/GPLv3.txt for more information.
+ */
+
+#pragma once
+
+#include "Swiften/Parser/GenericPayloadParserFactory.h"
+#include "Swiften/Parser/PayloadParsers/CapsInfoParser.h"
+
+namespace Swift {
+	class CapsInfoParserFactory : public GenericPayloadParserFactory<CapsInfoParser> {
+		public:
+			CapsInfoParserFactory() : GenericPayloadParserFactory<CapsInfoParser>("c", "http://jabber.org/protocol/caps") {}
+	};
+}
diff --git a/Swiften/Parser/PayloadParsers/FullPayloadParserFactoryCollection.cpp b/Swiften/Parser/PayloadParsers/FullPayloadParserFactoryCollection.cpp
index ce6c9f8..6294403 100644
--- a/Swiften/Parser/PayloadParsers/FullPayloadParserFactoryCollection.cpp
+++ b/Swiften/Parser/PayloadParsers/FullPayloadParserFactoryCollection.cpp
@@ -22,6 +22,7 @@
 #include "Swiften/Parser/PayloadParsers/StorageParserFactory.h"
 #include "Swiften/Parser/PayloadParsers/DiscoInfoParserFactory.h"
 #include "Swiften/Parser/PayloadParsers/DiscoItemsParserFactory.h"
+#include "Swiften/Parser/PayloadParsers/CapsInfoParserFactory.h"
 #include "Swiften/Parser/PayloadParsers/SecurityLabelParserFactory.h"
 #include "Swiften/Parser/PayloadParsers/SecurityLabelsCatalogParserFactory.h"
 #include "Swiften/Parser/PayloadParsers/FormParserFactory.h"
@@ -49,6 +50,7 @@ FullPayloadParserFactoryCollection::FullPayloadParserFactoryCollection() {
 	factories_.push_back(shared_ptr<PayloadParserFactory>(new RosterParserFactory()));
 	factories_.push_back(shared_ptr<PayloadParserFactory>(new DiscoInfoParserFactory()));
 	factories_.push_back(shared_ptr<PayloadParserFactory>(new DiscoItemsParserFactory()));
+	factories_.push_back(shared_ptr<PayloadParserFactory>(new CapsInfoParserFactory()));
 	factories_.push_back(shared_ptr<PayloadParserFactory>(new ResourceBindParserFactory()));
 	factories_.push_back(shared_ptr<PayloadParserFactory>(new StartSessionParserFactory()));
 	factories_.push_back(shared_ptr<PayloadParserFactory>(new SecurityLabelParserFactory()));
diff --git a/Swiften/Parser/SConscript b/Swiften/Parser/SConscript
index 4ab70a4..b9119d0 100644
--- a/Swiften/Parser/SConscript
+++ b/Swiften/Parser/SConscript
@@ -21,6 +21,7 @@ sources = [
 		"PayloadParsers/BodyParser.cpp",
 		"PayloadParsers/SubjectParser.cpp",
 		"PayloadParsers/ChatStateParser.cpp",
+		"PayloadParsers/CapsInfoParser.cpp",
 		"PayloadParsers/DiscoInfoParser.cpp",
 		"PayloadParsers/DiscoItemsParser.cpp",
 		"PayloadParsers/ErrorParser.cpp",
diff --git a/Swiften/Queries/GenericRequest.h b/Swiften/Queries/GenericRequest.h
index be6a88d..72bbcbd 100644
--- a/Swiften/Queries/GenericRequest.h
+++ b/Swiften/Queries/GenericRequest.h
@@ -26,6 +26,11 @@ namespace Swift {
 				onResponse(boost::dynamic_pointer_cast<PAYLOAD_TYPE>(payload), error);
 			}
 
+		protected:
+			boost::shared_ptr<PAYLOAD_TYPE> getPayloadGeneric() const {
+				return boost::dynamic_pointer_cast<PAYLOAD_TYPE>(getPayload());
+			}
+
 		public:
 			boost::signal<void (boost::shared_ptr<PAYLOAD_TYPE>, const boost::optional<ErrorPayload>&)> onResponse;
 	};
diff --git a/Swiften/Queries/Request.h b/Swiften/Queries/Request.h
index d0fd8c6..450e311 100644
--- a/Swiften/Queries/Request.h
+++ b/Swiften/Queries/Request.h
@@ -38,6 +38,10 @@ namespace Swift {
 				payload_ = p;
 			}
 
+			boost::shared_ptr<Payload> getPayload() const {
+				return payload_;
+			}
+
 			virtual void handleResponse(boost::shared_ptr<Payload>, boost::optional<ErrorPayload>) = 0;
 
 		private:
diff --git a/Swiften/Queries/Requests/GetDiscoInfoRequest.h b/Swiften/Queries/Requests/GetDiscoInfoRequest.h
index fd4d9f6..9ec1050 100644
--- a/Swiften/Queries/Requests/GetDiscoInfoRequest.h
+++ b/Swiften/Queries/Requests/GetDiscoInfoRequest.h
@@ -15,5 +15,10 @@ namespace Swift {
 			GetDiscoInfoRequest(const JID& jid, IQRouter* router) :
 					GenericRequest<DiscoInfo>(IQ::Get, jid, boost::shared_ptr<DiscoInfo>(new DiscoInfo()), router) {
 			}
+
+			GetDiscoInfoRequest(const JID& jid, const String& node, IQRouter* router) :
+					GenericRequest<DiscoInfo>(IQ::Get, jid, boost::shared_ptr<DiscoInfo>(new DiscoInfo()), router) {
+				getPayloadGeneric()->setNode(node);
+			}
 	};
 }
diff --git a/Swiften/SConscript b/Swiften/SConscript
index 3d82cfa..a9e3a01 100644
--- a/Swiften/SConscript
+++ b/Swiften/SConscript
@@ -36,7 +36,6 @@ if env["SCONS_STAGE"] == "build" :
 			"Compress/ZLibCodecompressor.cpp",
 			"Compress/ZLibDecompressor.cpp",
 			"Compress/ZLibCompressor.cpp",
-			"Disco/CapsInfoGenerator.cpp",
 			"Elements/DiscoInfo.cpp",
 			"Elements/Element.cpp",
 			"Elements/IQ.cpp",
@@ -125,6 +124,7 @@ if env["SCONS_STAGE"] == "build" :
 			"EventLoop",
 			"Parser",
 			"JID",
+			"Disco",
 			"VCards",
 			"Network",
 			"History",
@@ -153,6 +153,8 @@ if env["SCONS_STAGE"] == "build" :
 			File("Compress/UnitTest/ZLibCompressorTest.cpp"),
 			File("Compress/UnitTest/ZLibDecompressorTest.cpp"),
 			File("Disco/UnitTest/CapsInfoGeneratorTest.cpp"),
+			File("Disco/UnitTest/CapsManagerTest.cpp"),
+			File("Disco/UnitTest/EntityCapsManagerTest.cpp"),
 			File("Elements/UnitTest/IQTest.cpp"),
 			File("Elements/UnitTest/StanzaTest.cpp"),
 			File("Elements/UnitTest/StanzasTest.cpp"),
diff --git a/Swiften/VCards/VCardFileStorage.cpp b/Swiften/VCards/VCardFileStorage.cpp
index 66bae04..b4536f9 100644
--- a/Swiften/VCards/VCardFileStorage.cpp
+++ b/Swiften/VCards/VCardFileStorage.cpp
@@ -39,7 +39,12 @@ boost::shared_ptr<VCard> VCardFileStorage::getVCard(const JID& jid) const {
 void VCardFileStorage::setVCard(const JID& jid, boost::shared_ptr<VCard> v) {
 	boost::filesystem::path vcardPath(getVCardPath(jid));
 	if (!boost::filesystem::exists(vcardPath.parent_path())) {
-		boost::filesystem::create_directories(vcardPath.parent_path());
+		try {
+			boost::filesystem::create_directories(vcardPath.parent_path());
+		}
+		catch (const boost::filesystem::filesystem_error& e) {
+			std::cerr << "ERROR: " << e.what() << std::endl;
+		}
 	}
 	boost::filesystem::ofstream file(getVCardPath(jid));
 	file << VCardSerializer().serializePayload(v);
-- 
cgit v0.10.2-6-g49f6