From a3827fe5d02640ff850343ce54a22f4852ce7522 Mon Sep 17 00:00:00 2001
From: Yann Leboulanger <asterix@lagaule.org>
Date: Sun, 20 Apr 2008 22:58:47 +0000
Subject: [PATCH] new XEP-0115 implementation (version 1.5)

---
 configure.ac                      |   2 +-
 src/common/caps.py                | 173 +++++++++++++-----------------
 src/common/check_paths.py         |   5 +-
 src/common/connection_handlers.py |  65 ++---------
 src/common/contacts.py            |   4 +-
 src/common/defs.py                |   2 +-
 src/common/gajim.py               |  16 +++
 src/common/helpers.py             |  94 +++++++++++++++-
 src/common/logger.py              |  53 +++++----
 src/common/optparser.py           |  29 +++++
 src/common/xmpp/transports_nb.py  |  12 +--
 src/config.py                     |  12 +++
 src/gajim.py                      |   2 +
 13 files changed, 274 insertions(+), 195 deletions(-)

diff --git a/configure.ac b/configure.ac
index 6188951d27..a68e40e8ba 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,5 +1,5 @@
 AC_INIT([Gajim - A Jabber Instant Messager],
-		[0.11.4.3-svn],[http://trac.gajim.org/],[gajim])
+		[0.11.4.4-svn],[http://trac.gajim.org/],[gajim])
 AC_PREREQ([2.59])
 AM_INIT_AUTOMAKE([1.8])
 AC_CONFIG_HEADER(config.h)
diff --git a/src/common/caps.py b/src/common/caps.py
index 080c285034..585820e34d 100644
--- a/src/common/caps.py
+++ b/src/common/caps.py
@@ -20,6 +20,7 @@ from itertools import *
 import xmpp
 import xmpp.features_nb
 import gajim
+import helpers
 
 class CapsCache(object):
 	''' This object keeps the mapping between caps data and real disco
@@ -93,17 +94,30 @@ class CapsCache(object):
 
 		class CacheItem(object):
 			''' TODO: logging data into db '''
-			def __init__(ciself, node, version, ext=None):
+			def __init__(ciself, hash_method, hash):
 				# cached into db
-				ciself.node = node
-				ciself.version = version
-				ciself.features = set()
-				ciself.ext = ext
-				ciself.exts = {}
-
-				# set of tuples: (category, type, name)
-				# (dictionaries are not hashable, so cannot be in sets)
-				ciself.identities = set()
+				ciself.hash_method = hash_method
+				ciself.hash = hash
+
+				@property
+				def features():
+					def fget(self):
+						return self.getAttr('features')
+					def fset(self, value):
+						list_ = []
+						for feature in value:
+							list_.append(self.__names.setdefault(feature, feature))
+						self.setAttr('features', list_)
+
+				@property
+				def identities():
+					def fget(self):
+						return self.getAttr('identities')
+					def fset(self, value):
+						list_ = []
+						for identity in value:
+							list_.append(self.__names.setdefault(identity, identity))
+						self.setAttr('identities', list_)
 
 				# not cached into db:
 				# have we sent the query?
@@ -112,53 +126,21 @@ class CapsCache(object):
 				# 2 == got the answer
 				ciself.queried = 0
 
-			class CacheQuery(object):
-				def __init__(cqself, proxied):
-					cqself.proxied=proxied
-
-				def __getattr__(cqself, obj):
-					if obj!='exts': return getattr(cqself.proxied[0], obj)
-					return set(chain(ci.features for ci in cqself.proxied))
-
-			def __getitem__(ciself, exts):
-				if not exts:	# (), [], None, False, whatever
-					return ciself
-				if isinstance(exts, basestring):
-					exts=(exts,)
-				if len(exts)==1:
-					ext=exts[0]
-					if ext in ciself.exts:
-						return ciself.exts[ext]
-					x=CacheItem(ciself.node, ciself.version, ext)
-					ciself.exts[ext]=x
-					return x
-				proxied = [ciself]
-				proxied.extend(ciself[(e,)] for e in exts)
-				return ciself.CacheQuery(proxied)
-
 			def update(ciself, identities, features):
 				# NOTE: self refers to CapsCache object, not to CacheItem
-				self.identities=identities
-				self.features=features
-				self.logger.add_caps_entry(
-					ciself.node, ciself.version, ciself.ext,
+				ciself.identities=identities
+				ciself.features=features
+				self.logger.add_caps_entry(ciself.hash_method, ciself.hash,
 					identities, features)
 
 		self.__CacheItem = CacheItem
 
 		# prepopulate data which we are sure of; note: we do not log these info
-		gajimnode = 'http://gajim.org/caps'
-
-		gajimcaps=self[(gajimnode, '0.11.1')]
-		gajimcaps.category='client'
-		gajimcaps.type='pc'
-		gajimcaps.features=set((xmpp.NS_BYTESTREAM, xmpp.NS_SI,
-			xmpp.NS_FILE, xmpp.NS_MUC, xmpp.NS_COMMANDS,
-			xmpp.NS_DISCO_INFO, xmpp.NS_PING, xmpp.NS_TIME_REVISED))
-		gajimcaps['cstates'].features=set((xmpp.NS_CHATSTATES,))
-		gajimcaps['xhtml'].features=set((xmpp.NS_XHTML_IM,))
 
-		# TODO: older gajim versions
+		gajimcaps = self[('sha-1', gajim.caps_hash)]
+		gajimcaps.identities = [gajim.gajim_identity]
+		gajimcaps.features = gajim.gajim_common_features + \
+			gajim.gajim_optional_features
 
 		# start logging data from the net
 		self.logger = logger
@@ -166,40 +148,32 @@ class CapsCache(object):
 	def load_from_db(self):
 		# get data from logger...
 		if self.logger is not None:
-			for node, ver, ext, identities, features in self.logger.iter_caps_data():
-				x=self[(node, ver, ext)]
-				x.identities=identities
-				x.features=features
-				x.queried=2
+			for hash_method, hash, identities, features in \
+			self.logger.iter_caps_data():
+				x = self[(hash_method, hash)]
+				x.identities = identities
+				x.features = features
+				x.queried = 2
 
 	def __getitem__(self, caps):
-		node_version = caps[:2]
-		if node_version in self.__cache:
-			return self.__cache[node_version][caps[2]]
-		node, version = self.__names.setdefault(caps[0], caps[0]), caps[1]
-		x=self.__CacheItem(node, version)
-		self.__cache[(node, version)]=x
+		if caps in self.__cache:
+			return self.__cache[caps]
+		hash_method, hash = caps[0], caps[1]
+		x = self.__CacheItem(hash_method, hash)
+		self.__cache[(hash_method, hash)] = x
 		return x
 
-	def preload(self, account, jid, node, ver, exts):
+	def preload(self, con, jid, node, hash_method, hash):
 		''' Preload data about (node, ver, exts) caps using disco
 		query to jid using proper connection. Don't query if
 		the data is already in cache. '''
-		q=self[(node, ver, ())]
-		qq=q
+		q = self[(hash_method, hash)]
 
 		if q.queried==0:
-			# do query for bare node+version pair
+			# do query for bare node+hash pair
 			# this will create proper object
 			q.queried=1
-			account.discoverInfo(jid, '%s#%s' % (node, ver))
-
-		for ext in exts:
-			qq=q[ext]
-			if qq.queried==0:
-				# do query for node+version+ext triple
-				qq.queried=1
-				account.discoverInfo(jid, '%s#%s' % (node, ext))
+			con.discoverInfo(jid, '%s#%s' % (node, hash))
 
 gajim.capscache = CapsCache(gajim.logger)
 
@@ -210,20 +184,15 @@ class ConnectionCaps(object):
 		for xmpp registered in connection_handlers.py'''
 
 		# get the caps element
-		caps=presence.getTag('c')
-		if not caps: return
+		caps = presence.getTag('c')
+		if not caps:
+			return
 
-		node, ver=caps['node'], caps['ver']
-		if node is None or ver is None:
+		hash_method, node, hash = caps['hash'], caps['node'], caps['ver']
+		if hash_method is None or node is None or hash is None:
 			# improper caps in stanza, ignoring
 			return
 
-		try:
-			exts=caps['ext'].split(' ')
-		except AttributeError:
-			# no exts means no exts, a perfectly valid case
-			exts=[]
-
 		# we will put these into proper Contact object and ask
 		# for disco... so that disco will learn how to interpret
 		# these caps
@@ -231,7 +200,7 @@ class ConnectionCaps(object):
 		jid=str(presence.getFrom())
 
 		# start disco query...
-		gajim.capscache.preload(self, jid, node, ver, exts)
+		gajim.capscache.preload(self, jid, node, hash_method, hash)
 
 		contact=gajim.contacts.get_contact_from_full_jid(self.name, jid)
 		if contact in [None, []]:
@@ -240,25 +209,31 @@ class ConnectionCaps(object):
 			contact = contact[0]
 
 		# overwriting old data
-		contact.caps_node=node
-		contact.caps_ver=ver
-		contact.caps_exts=exts
+		contact.caps_node = node
+		contact.caps_hash_method = hash_method
+		contact.caps_hash = hash
 
 	def _capsDiscoCB(self, jid, node, identities, features, data):
-		contact=gajim.contacts.get_contact_from_full_jid(self.name, jid)
-		if not contact: return
-		if not contact.caps_node: return # we didn't asked for that?
-		if not node.startswith(contact.caps_node+'#'): return
-		node, ext = node.split('#', 1)
-		if ext==contact.caps_ver:	# this can be also version (like '0.9')
-			exts=None
-		else:
-			exts=(ext,)
+		contact = gajim.contacts.get_contact_from_full_jid(self.name, jid)
+		if not contact:
+			return
+		if not contact.caps_node:
+			return # we didn't asked for that?
+		if not node.startswith(contact.caps_node + '#'):
+			return
+		node, hash = node.split('#', 1)
+		computed_hash = helpers.compute_caps_hash(identities, features,
+			contact.caps_hash_method)
+		if computed_hash != hash:
+			# wrong hash, forget it
+			contact.caps_node = ''
+			contact.caps_hash_method = ''
+			contact.caps_hash = ''
+			return
 
 		# if we don't have this info already...
-		caps=gajim.capscache[(node, contact.caps_ver, exts)]
-		if caps.queried==2: return
+		caps = gajim.capscache[(contact.caps_hash_method, hash)]
+		if caps.queried == 2:
+			return
 
-		identities=set((i['category'], i['type'], i.get('name')) for i in identities)
 		caps.update(identities, features)
-
diff --git a/src/common/check_paths.py b/src/common/check_paths.py
index cd77e07674..970c5a4b21 100644
--- a/src/common/check_paths.py
+++ b/src/common/check_paths.py
@@ -82,9 +82,8 @@ def create_log_db():
 		CREATE INDEX idx_logs_jid_id_kind ON logs (jid_id, kind);
 
 		CREATE TABLE caps_cache (
-			node TEXT,
-			ver TEXT,
-			ext TEXT,
+			hash_method TEXT,
+			hash TEXT,
 			data BLOB);
 
 		CREATE TABLE IF NOT EXISTS rooms_last_message_time(
diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py
index e7f94916dd..342df050b4 100644
--- a/src/common/connection_handlers.py
+++ b/src/common/connection_handlers.py
@@ -667,7 +667,7 @@ class ConnectionDisco:
 			common.xmpp.NS_DISCO, frm = to)
 		iq.setAttr('id', id)
 		query = iq.setTag('query')
-		query.setAttr('node','http://gajim.org/caps#' + gajim.version.split('-',
+		query.setAttr('node','http://gajim.org#' + gajim.version.split('-',
 			1)[0])
 		for f in (common.xmpp.NS_BYTESTREAM, common.xmpp.NS_SI,
 		common.xmpp.NS_FILE, common.xmpp.NS_COMMANDS):
@@ -744,55 +744,17 @@ class ConnectionDisco:
 			q = iq.getTag('query')
 			if node:
 				q.setAttr('node', node)
-			q.addChild('identity', attrs = {'type': 'pc', 'category': 'client',
-				'name': 'Gajim'})
+			q.addChild('identity', attrs = gajim.gajim_identity)
 			extension = None
 			if node and node.find('#') != -1:
 				extension = node[node.index('#') + 1:]
-			client_version = 'http://gajim.org/caps#' + gajim.version.split('-',
-				1)[0]
+			client_version = 'http://gajim.org#' + gajim.caps_hash
 
 			if node in (None, client_version):
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_BYTESTREAM})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_SI})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_FILE})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_MUC})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_MUC_USER})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_MUC_ADMIN})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_MUC_OWNER})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_MUC_CONFIG})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_COMMANDS})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_DISCO_INFO})
-				q.addChild('feature', attrs = {'var': 'ipv6'})
-				q.addChild('feature', attrs = {'var': 'jabber:iq:gateway'})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_LAST})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_PRIVACY})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_PRIVATE})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_REGISTER})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_VERSION})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_DATA})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_ENCRYPTED})
-				q.addChild('feature', attrs = {'var': 'msglog'})
-				q.addChild('feature', attrs = {'var': 'sslc2s'})
-				q.addChild('feature', attrs = {'var': 'stringprep'})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_PING})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_ACTIVITY})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_ACTIVITY + '+notify'})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_TUNE})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_TUNE + '+notify'})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_MOOD})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_MOOD + '+notify'})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_ESESSION_INIT})
-
-			if (node is None or extension == 'cstates') and gajim.config.get('outgoing_chat_state_notifactions') != 'disabled':
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_CHATSTATES})
-
-			if (node is None or extension == 'xhtml') and not gajim.config.get('ignore_incoming_xhtml'):
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_XHTML_IM})
-
-			if node is None:
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_PING})
-				q.addChild('feature', attrs = {'var': common.xmpp.NS_TIME_REVISED})
+				for f in gajim.gajim_common_features:
+					q.addChild('feature', attrs = {'var': f})
+				for f in gajim.gajim_optional_features:
+					q.addChild('feature', attrs = {'var': f})
 
 			if q.getChildren():
 				self.connection.send(iq)
@@ -892,16 +854,9 @@ class ConnectionVcard:
 	def add_caps(self, p):
 		''' advertise our capabilities in presence stanza (xep-0115)'''
 		c = p.setTag('c', namespace = common.xmpp.NS_CAPS)
-		c.setAttr('node', 'http://gajim.org/caps')
-		ext = []
-		if not gajim.config.get('ignore_incoming_xhtml'):
-			ext.append('xhtml')
-		if gajim.config.get('outgoing_chat_state_notifactions') != 'disabled':
-			ext.append('cstates')
-
-		if len(ext):
-			c.setAttr('ext', ' '.join(ext))
-		c.setAttr('ver', gajim.version.split('-', 1)[0])
+		c.setAttr('hash', 'sha-1')
+		c.setAttr('node', 'http://gajim.org')
+		c.setAttr('ver', gajim.caps_hash)
 		return p
 	
 	def node_to_dict(self, node):
diff --git a/src/common/contacts.py b/src/common/contacts.py
index 1b9937e55d..63724c238f 100644
--- a/src/common/contacts.py
+++ b/src/common/contacts.py
@@ -44,8 +44,8 @@ class Contact:
 		# Capabilities; filled by caps.py/ConnectionCaps object
 		# every time it gets these from presence stanzas
 		self.caps_node = None
-		self.caps_ver = None
-		self.caps_exts = None
+		self.caps_hash_method = None
+		self.caps_hash = None
 
 		# please read xep-85 http://www.xmpp.org/extensions/xep-0085.html
 		# we keep track of xep85 support with the peer by three extra states:
diff --git a/src/common/defs.py b/src/common/defs.py
index d3ddd971ff..f6a742f600 100644
--- a/src/common/defs.py
+++ b/src/common/defs.py
@@ -2,7 +2,7 @@ docdir = '../'
 
 datadir = '../'
 
-version = '0.11.4.3-svn'
+version = '0.11.4.4-svn'
 
 import sys, os.path
 for base in ('.', 'common'):
diff --git a/src/common/gajim.py b/src/common/gajim.py
index a758982c60..978f67c81c 100644
--- a/src/common/gajim.py
+++ b/src/common/gajim.py
@@ -27,6 +27,7 @@ import locale
 import config
 from contacts import Contacts
 from events import Events
+import xmpp
 
 try:
 	import defs
@@ -164,6 +165,21 @@ else:
 	if system('gpg -h >/dev/null 2>&1'):
 		HAVE_GPG = False
 
+gajim_identity = {'type': 'pc', 'category': 'client', 'name': 'Gajim'}
+gajim_common_features = [xmpp.NS_BYTESTREAM, xmpp.NS_SI,
+	xmpp.NS_FILE, xmpp.NS_MUC, xmpp.NS_MUC_USER,
+	xmpp.NS_MUC_ADMIN, xmpp.NS_MUC_OWNER,
+	xmpp.NS_MUC_CONFIG, xmpp.NS_COMMANDS,
+	xmpp.NS_DISCO_INFO, 'ipv6', 'jabber:iq:gateway', xmpp.NS_LAST,
+	xmpp.NS_PRIVACY, xmpp.NS_PRIVATE, xmpp.NS_REGISTER,
+	xmpp.NS_VERSION, xmpp.NS_DATA, xmpp.NS_ENCRYPTED,
+	'msglog', 'sslc2s', 'stringprep', xmpp.NS_PING,
+	xmpp.NS_TIME_REVISED]
+# Optional features gajim supports
+gajim_optional_features = []
+
+caps_hash = ''
+
 def get_nick_from_jid(jid):
 	pos = jid.find('@')
 	return jid[:pos]
diff --git a/src/common/helpers.py b/src/common/helpers.py
index 2f597645e5..4f7efe09a2 100644
--- a/src/common/helpers.py
+++ b/src/common/helpers.py
@@ -30,6 +30,8 @@ import urllib
 import errno
 import select
 import sha
+import hashlib
+import base64
 import sys
 from encodings.punycode import punycode_encode
 from encodings import idna
@@ -38,7 +40,7 @@ import gajim
 from i18n import Q_
 from i18n import ngettext
 from xmpp_stringprep import nodeprep, resourceprep, nameprep
-
+import xmpp
 
 if sys.platform == 'darwin':
 	from osx import nsapp
@@ -1199,3 +1201,93 @@ def prepare_and_validate_gpg_keyID(account, jid, keyID):
 			keyID = 'UNKNOWN'
 	return keyID
 
+def sort_identities_func(i1, i2):
+	cat1 = i1['category']
+	cat2 = i2['category']
+	if cat1 < cat2:
+		return -1
+	if cat1 > cat2:
+		return 1
+	if i1.has_key('type'):
+		type1 = i1['type']
+	else:
+		type1 = ''
+	if i2.has_key('type'):
+		type2 = i2['type']
+	else:
+		type2 = ''
+	if type1 < type2:
+		return -1
+	if type1 > type2:
+		return 1
+	if i1.has_key('xml:lang'):
+		lang1 = i1['xml:lang']
+	else:
+		lang1 = ''
+	if i2.has_key('xml:lang'):
+		lang2 = i2['xml:lang']
+	else:
+		lang2 = ''
+	if lang1 < lang2:
+		return -1
+	if lang1 > lang2:
+		return 1
+	return 0
+
+def compute_caps_hash(identities, features, hash_method='sha-1'):
+	S = ''
+	identities.sort(cmp=sort_identities_func)
+	for i in identities:
+		c = i['category']
+		if i.has_key('type'):
+			type_ = i['type']
+		else:
+			type_ = ''
+		if i.has_key('xml:lang'):
+			lang = i['xml:lang']
+		else:
+			lang = ''
+		if i.has_key('name'):
+			name = i['name']
+		else:
+			name = ''
+		S += '%s/%s/%s/%s<' % (c, type_, lang, name)
+	features.sort()
+	for f in features:
+		S += '%s<' % f
+	if hash_method == 'sha-1':
+		hash = hashlib.sha1(S)
+	elif hash_method == 'md5':
+		hash = hashlib.md5(S)
+	else:
+		return ''
+	return base64.b64encode(hash.digest())
+
+def update_optional_features():
+	gajim.gajim_optional_features = []
+	if gajim.config.get('publish_mood'):
+		gajim.gajim_optional_features.append(xmpp.NS_MOOD)
+	if gajim.config.get('subscribe_mood'):
+		gajim.gajim_optional_features.append(xmpp.NS_MOOD + '+notify')
+	if gajim.config.get('publish_activity'):
+		gajim.gajim_optional_features.append(xmpp.NS_ACTIVITY)
+	if gajim.config.get('subscribe_activity'):
+		gajim.gajim_optional_features.append(xmpp.NS_ACTIVITY + '+notify')
+	if gajim.config.get('publish_tune'):
+		gajim.gajim_optional_features.append(xmpp.NS_TUNE)
+	if gajim.config.get('subscribe_tune'):
+		gajim.gajim_optional_features.append(xmpp.NS_TUNE + '+notify')
+	if gajim.config.get('outgoing_chat_state_notifactions') != 'disabled':
+		gajim.gajim_optional_features.append(xmpp.NS_CHATSTATES)
+	if not gajim.config.get('ignore_incoming_xhtml'):
+		gajim.gajim_optional_features.append(xmpp.NS_XHTML_IM)
+	if gajim.HAVE_PYCRYPTO:
+		gajim.gajim_optional_features.append(xmpp.NS_ESESSION_INIT)
+	gajim.caps_hash = compute_caps_hash([gajim.gajim_identity],
+		gajim.gajim_common_features + gajim.gajim_optional_features)
+	# re-send presence with new hash
+	for account in gajim.connections:
+		connected = gajim.connections[account].connected
+		if connected > 1 and gajim.SHOW_LIST[connected] != 'invisible':
+			gajim.connections[account].change_status(gajim.SHOW_LIST[connected],
+				gajim.connections[account].status)
diff --git a/src/common/logger.py b/src/common/logger.py
index bdd9a549e8..55301be5c1 100644
--- a/src/common/logger.py
+++ b/src/common/logger.py
@@ -698,60 +698,59 @@ class Logger:
 		# to get that data without trying to convert it to unicode
 		#tmp, self.con.text_factory = self.con.text_factory, str
 		try:
-			self.cur.execute('''SELECT node, ver, ext, data FROM caps_cache;''');
+			self.cur.execute('SELECT hash_method, hash, data FROM caps_cache;');
 		except sqlite.OperationalError:
 			# might happen when there's no caps_cache table yet
 			# -- there's no data to read anyway then
 			#self.con.text_factory = tmp
 			return
 		#self.con.text_factory = tmp
-
-		for node, ver, ext, data in self.cur:
+		for hash_method, hash, data in self.cur:
 			# for each row: unpack the data field
 			# (format: (category, type, name, category, type, name, ...
 			#   ..., 'FEAT', feature1, feature2, ...).join(' '))
 			# NOTE: if there's a need to do more gzip, put that to a function
-			data=GzipFile(fileobj=StringIO(str(data))).read().split('\0')
+			data = GzipFile(fileobj=StringIO(str(data))).read().split('\0')
 			i=0
-			identities=set()
-			features=set()
-			while i<(len(data)-2) and data[i]!='FEAT':
-				category=data[i]
-				type=data[i+1]
-				name=data[i+2]
-				identities.add((category,type,name))
-				i+=3
+			identities = list()
+			features = list()
+			while i < (len(data) - 3) and data[i] != 'FEAT':
+				category = data[i]
+				type_ = data[i + 1]
+				lang = data[i + 2]
+				name = data[i + 3]
+				identities.append({'category': category, 'type': type_,
+					'xml:lang': lang, 'name': name})
+				i += 4
 			i+=1
-			while i<len(data):
-				features.add(data[i])
-				i+=1
-			if not ext: ext=None	# to make '' a None
+			while i < len(data):
+				features.append(data[i])
+				i += 1
 
 			# yield the row
-			yield node, ver, ext, identities, features
+			yield hash_method, hash, identities, features
 
-	def add_caps_entry(self, node, ver, ext, identities, features):
+	def add_caps_entry(self, hash_method, hash, identities, features):
 		data=[]
 		for identity in identities:
 			# there is no FEAT category
-			if identity[0]=='FEAT': return
-			if len(identity)<2 or not identity[2]:
-				data.extend((identity[0], identity[1], ''))
-			else:
-				data.extend(identity)
+			if identity['category'] == 'FEAT':
+				return
+			data.extend((identity.get('category'), identity.get('type', ''),
+				identity.get('xml:lang', ''), identity.get('name', '')))
 		data.append('FEAT')
 		data.extend(features)
 		data = '\0'.join(data)
 		# if there's a need to do more gzip, put that to a function
 		string = StringIO()
-		gzip=GzipFile(fileobj=string, mode='w')
+		gzip = GzipFile(fileobj=string, mode='w')
 		gzip.write(data)
 		gzip.close()
 		data = string.getvalue()
 		self.cur.execute('''
-			INSERT INTO caps_cache ( node, ver, ext, data )
-			VALUES (?, ?, ?, ?);
-			''', (node, ver, ext, buffer(data))) # (1) -- note above
+			INSERT INTO caps_cache ( hash_method, hash, data )
+			VALUES (?, ?, ?);
+			''', (hash_method, hash, buffer(data))) # (1) -- note above
 		try:
 			self.con.commit()
 		except sqlite.OperationalError, e:
diff --git a/src/common/optparser.py b/src/common/optparser.py
index c6952dfc21..5d62f8dd71 100644
--- a/src/common/optparser.py
+++ b/src/common/optparser.py
@@ -180,6 +180,8 @@ class OptionsParser:
 			self.update_config_to_01142()
 		if old < [0, 11, 4, 3] and new >= [0, 11, 4, 3]:
 			self.update_config_to_01143()
+		if old < [0, 11, 4, 4] and new >= [0, 11, 4, 4]:
+			self.update_config_to_01144()
 
 		gajim.logger.init_vars()
 		gajim.config.set('version', new_version)
@@ -566,3 +568,30 @@ class OptionsParser:
 			pass
 		con.close()
 		gajim.config.set('version', '0.11.4.3')
+
+	def update_config_to_01144(self):
+		back = os.getcwd()
+		os.chdir(logger.LOG_DB_FOLDER)
+		con = sqlite.connect(logger.LOG_DB_FILE)
+		os.chdir(back)
+		cur = con.cursor()
+		try:
+			cur.executescript('DROP TABLE caps_cache;')
+			con.commit()
+		except sqlite.OperationalError, e:
+			pass
+		try:
+			cur.executescript(
+				'''
+				CREATE TABLE caps_cache (
+					hash_method TEXT,
+					hash TEXT,
+					data BLOB
+				);
+				'''
+			)
+			con.commit()
+		except sqlite.OperationalError, e:
+			pass
+		con.close()
+		gajim.config.set('version', '0.11.4.4')
diff --git a/src/common/xmpp/transports_nb.py b/src/common/xmpp/transports_nb.py
index 83f199a4a5..8c18558089 100644
--- a/src/common/xmpp/transports_nb.py
+++ b/src/common/xmpp/transports_nb.py
@@ -33,7 +33,7 @@ import thread
 import logging
 log = logging.getLogger('gajim.c.x.transports_nb')
 
-from common import gajim
+import common.gajim
 
 USE_PYOPENSSL = False
 
@@ -755,16 +755,16 @@ class NonBlockingTLS(PlugIn):
 		#tcpsock._sslContext = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
 		tcpsock.ssl_errnum = 0
 		tcpsock._sslContext.set_verify(OpenSSL.SSL.VERIFY_PEER, self._ssl_verify_callback)
-		cacerts = os.path.join(gajim.DATA_DIR, 'other', 'cacerts.pem')
+		cacerts = os.path.join(common.gajim.DATA_DIR, 'other', 'cacerts.pem')
 		try:
 			tcpsock._sslContext.load_verify_locations(cacerts)
 		except:
 			log.warning('Unable to load SSL certificats from file %s' % \
 				os.path.abspath(cacerts))
 		# load users certs
-		if os.path.isfile(gajim.MY_CACERTS):
+		if os.path.isfile(common.gajim.MY_CACERTS):
 			store = tcpsock._sslContext.get_cert_store()
-			f = open(gajim.MY_CACERTS)
+			f = open(common.gajim.MY_CACERTS)
 			lines = f.readlines()
 			i = 0
 			begin = -1
@@ -779,11 +779,11 @@ class NonBlockingTLS(PlugIn):
 						store.add_cert(X509cert)
 					except OpenSSL.crypto.Error, exception_obj:
 						log.warning('Unable to load a certificate from file %s: %s' %\
-							(gajim.MY_CACERTS, exception_obj.args[0][0][2]))
+							(common.gajim.MY_CACERTS, exception_obj.args[0][0][2]))
 					except:
 						log.warning(
 							'Unknown error while loading certificate from file %s' % \
-							gajim.MY_CACERTS)
+							common.gajim.MY_CACERTS)
 					begin = -1
 				i += 1
 		tcpsock._sslObj = OpenSSL.SSL.Connection(tcpsock._sslContext, tcpsock._sock)
diff --git a/src/config.py b/src/config.py
index 438688136e..c6796f0d81 100644
--- a/src/config.py
+++ b/src/config.py
@@ -538,6 +538,7 @@ class PreferencesWindow:
 				if gajim.connections[account].pep_supported:
 					pep.user_send_mood(account, '')
 		self.on_checkbutton_toggled(widget, 'publish_mood')
+		helpers.update_optional_features()
 
 	def on_publish_activity_checkbutton_toggled(self, widget):
 		if widget.get_active() == False:
@@ -545,6 +546,7 @@ class PreferencesWindow:
 				if gajim.connections[account].pep_supported:
 					pep.user_send_activity(account, '')
 		self.on_checkbutton_toggled(widget, 'publish_activity')
+		helpers.update_optional_features()
 
 	def on_publish_tune_checkbutton_toggled(self, widget):
 		if widget.get_active() == False:
@@ -552,17 +554,21 @@ class PreferencesWindow:
 				if gajim.connections[account].pep_supported:
 					pep.user_send_tune(account, '')
 		self.on_checkbutton_toggled(widget, 'publish_tune')
+		helpers.update_optional_features()
 		gajim.interface.roster.enable_syncing_status_msg_from_current_music_track(
 			widget.get_active())
 
 	def on_subscribe_mood_checkbutton_toggled(self, widget):
 		self.on_checkbutton_toggled(widget, 'subscribe_mood')
+		helpers.update_optional_features()
 
 	def on_subscribe_activity_checkbutton_toggled(self, widget):
 		self.on_checkbutton_toggled(widget, 'subscribe_activity')
+		helpers.update_optional_features()
 
 	def on_subscribe_tune_checkbutton_toggled(self, widget):
 		self.on_checkbutton_toggled(widget, 'subscribe_tune')
+		helpers.update_optional_features()
 
 	def on_sort_by_show_checkbutton_toggled(self, widget):
 		self.on_checkbutton_toggled(widget, 'sort_by_show')
@@ -625,6 +631,7 @@ class PreferencesWindow:
 
 	def on_xhtml_checkbutton_toggled(self, widget):
 		self.on_checkbutton_toggled(widget, 'ignore_incoming_xhtml')
+		helpers.update_optional_features()
 
 	def apply_speller(self):
 		for acct in gajim.connections:
@@ -719,12 +726,17 @@ class PreferencesWindow:
 
 	def on_outgoing_chat_states_combobox_changed(self, widget):
 		active = widget.get_active()
+		old_value = gajim.config.get('outgoing_chat_state_notifications')
 		if active == 0: # all
 			gajim.config.set('outgoing_chat_state_notifications', 'all')
 		elif active == 1: # only composing
 			gajim.config.set('outgoing_chat_state_notifications', 'composing_only')
 		else: # disabled
 			gajim.config.set('outgoing_chat_state_notifications', 'disabled')
+		new_value = gajim.config.get('outgoing_chat_state_notifications')
+		if 'disabled' in (old_value, new_value):
+			# we changed from disabled to sth else or vice versa
+			helpers.update_optional_features()
 
 	def on_displayed_chat_states_combobox_changed(self, widget):
 		active = widget.get_active()
diff --git a/src/gajim.py b/src/gajim.py
index 38e4f8d94c..1538d322b7 100755
--- a/src/gajim.py
+++ b/src/gajim.py
@@ -3106,6 +3106,8 @@ class Interface:
 		if gajim.config.get('autodetect_browser_mailer') or not cfg_was_read:
 			gtkgui_helpers.autodetect_browser_mailer()
 
+		helpers.update_optional_features()
+
 		if gajim.verbose:
 			gajim.log.setLevel(gajim.logging.DEBUG)
 		else:
-- 
GitLab