From 86798a56f0cbd096eea35dfe71a95d30b8f8a8e0 Mon Sep 17 00:00:00 2001
From: Tomasz Melcer <liori@exroot.org>
Date: Wed, 27 Jun 2007 00:51:12 +0000
Subject: [PATCH] Caps: Cache object

---
 src/common/caps.py     | 240 +++++++++++++++++++++++++++++++++++++++++
 src/common/contacts.py |   6 ++
 2 files changed, 246 insertions(+)
 create mode 100644 src/common/caps.py

diff --git a/src/common/caps.py b/src/common/caps.py
new file mode 100644
index 0000000000..e052fc119a
--- /dev/null
+++ b/src/common/caps.py
@@ -0,0 +1,240 @@
+##
+## Copyright (C) 2006 Gajim Team
+##
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published
+## by the Free Software Foundation; version 2 only.
+##
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+##
+
+#import xmpp
+#import logger
+#import gajim
+from itertools import *
+
+class CapsCache(object):
+	''' This object keeps the mapping between caps data and real disco
+	features they represent, and provides simple way to query that info. 
+	It is application-wide, that is there's one object for all
+	connections.
+	Goals:
+	 * handle storing/retrieving info from database
+	 * cache info in memory
+	 * expose simple interface
+	Properties:
+	 * one object for all connections (move to logger.py?)
+	 * store info efficiently (a set() of urls -- we can assume there won't be
+	   too much of these, ensure that (X,Y,Z1) and (X,Y,Z2) has different
+	   features.
+
+	Connections with other objects: (TODO)
+
+	Interface:
+
+	# object creation
+	>>> cc=CapsCache(logger_object)
+
+	>>> caps=('http://exodus.jabberstudio.org/caps', '0.9', None) # node, ver, ext
+	>>> muc='http://jabber.org/protocol/muc'
+	>>> chatstates='http://jabber.org/protocol/chatstates'
+
+	# retrieving data
+	>>> muc in cc[caps].features
+	True
+	>>> muc in cc[caps]
+	True
+	>>> chatstates in cc[caps]
+	False
+	>>> cc[caps].category
+	'client'
+	>>> cc[caps].type
+	'pc'
+	>>> x=cc[caps] # more efficient if making several queries for one set of caps
+	ATypicalBlackBoxObject
+	>>> muc in x
+	True
+	>>> x.node
+	'http://exodus.jabberstudio.org/caps'
+
+	# retrieving data (multiple exts case)
+	>>> caps=('http://gajim.org/caps', '0.9', ('csn', 'ft'))
+	>>> muc in cc[caps]
+	True
+
+	# setting data
+	>>> newcaps=('http://exodus.jabberstudio.org/caps', '0.9a', None)
+	>>> cc[newcaps].category='client'
+	>>> cc[newcaps].type='pc'
+	>>> cc[newcaps].features+=muc # same as:
+	>>> cc[newcaps]+=muc
+	>>> cc[newcaps]['csn']+=chatstates # adding data as if ext was 'csn'
+	# warning: no feature removal!
+	'''
+	def __init__(self, logger=None):
+		''' Create a cache for entity capabilities. '''
+		# our containers:
+		# __names is a string cache; every string long enough is given
+		#   another object, and we will have plenty of identical long
+		#   strings. therefore we can cache them
+		#   TODO: maybe put all known xmpp namespace strings here
+		#   (strings given in xmpppy)?
+		# __cache is a dictionary mapping: pair of node and version maps
+		#   to CapsCacheItem object
+		# __CacheItem is a class that stores data about particular
+		#   client (node/version pair)
+		self.__names = {}
+		self.__cache = {}
+		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))
+
+		class CacheItem(object):
+			''' TODO: logging data into db '''
+			def __init__(ciself, node, version, ext=None):
+				# cached into db
+				ciself.node = node
+				ciself.version = version
+				ciself.features = set()
+				ciself.exts = {}
+
+				ciself.identities = []
+				# reported as first... important?
+				ciself.category = None
+				ciself.type = None
+				ciself.name = None
+
+				ciself.cache = self
+
+				# not cached into db:
+				# have we sent the query?
+				# 0 == not queried
+				# 1 == queried
+				# 2 == got the answer
+				ciself.queried = 0
+
+			def __iadd__(ciself, newfeature):
+				newfeature=self.cache.__names.setdefault(newfeature, newfeature)
+				ciself.features.add(newfeature)
+
+			def __getitem__(ciself, exts):
+				if len(ext)==0:
+					return self
+				if len(ext)==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 = [self]
+				proxied.extend(ciself[(e,)] for e in ext)
+				return CacheQuery(proxied)
+
+		self.__CacheItem = CacheItem
+
+		# prepopulate data which we are sure of; note: we do not log these info
+		gajim = 'http://gajim.org/caps'
+
+		gajimcaps=self[(gajim, '0.11.1')]
+		gajimcaps.category='client'
+		gajimcaps.type='pc'
+		gajimcaps.features=set((common.xmpp.NS_BYTESTREAM, common.xmpp.NS_SI,
+			common.xmpp.NS_FILE, common.xmpp.NS_MUC, common.xmpp.NS_COMMANDS,
+			common.xmpp.NS_DISCO_INFO, common.xmpp.NS_PING, common.xmpp.NS_TIME_REVISED))
+		gajimcaps['cstates'].features=set((common.xmpp.NS_CHATSTATES,))
+		gajimcaps['xhtml'].features=set((common.xmpp.NS_XHTML_IM,))
+
+		# TODO: older gajim versions
+
+		# start logging data from the net
+		self.__logger = logger
+
+		# get data from logger...
+		if self.__logger is not None:
+			for node, version, category, type_, name in self.__logger.get_caps_cache():
+				x=self.__clients[(node, version)]
+				x.category=category
+				x.type=type_
+				x.name=name
+			for node, version, ext, feature in self.__logger.get_caps_features_cache():
+				self.__clients[(node, version)][ext]+=feature
+
+	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[caps[0]], caps[1]
+		x=self.__cache[(node, version)]=self.__CacheItem(node, version)
+		return x
+
+	def preload(self, connection, jid, node, ver, exts):
+		''' 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, ())]
+		if q.queried==0:
+			# do query for bare node+version pair
+			# this will create proper object
+			q.queried=1
+			def callback(identities, features):
+				q.queried=2
+				# TODO: put features and identities
+			xmpp.discoverInfo(con, jid, node='%s#%s' % (node, ver), callback)
+
+		for ext in exts:
+			qq=q[ext]
+			if qq.queried==0:
+				# do query for node+version+ext triple
+				qq.queried=1
+				def callback(identities, features):
+					qq.queried=2
+					# TODO: put features and identities
+				xmpp.discoverInfo(con, jid, node='%s#%s' % (node, ext))
+
+class ConnectionCaps(object):
+	''' This class highly depends on that it is a part of Connection class. '''
+	def _capsPresenceCB(self, con, presence):
+		''' Handle incoming presence stanzas... This is a callback
+		for xmpp registered in connection_handlers.py'''
+
+		# get the caps element
+		caps=presence.getTag('c')
+		if not caps: return
+
+		try:
+			node, ver=caps['node'], caps['ver']
+		except KeyError:
+			# improper caps in stanza, ignoring
+			return
+
+		try:
+			exts=caps['ext'].split(' ')
+		except KeyError:
+			# 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
+
+		jid=presence.getFrom()
+
+		# start disco query...
+		gajim.capscache.preload(self, connection, jid, node, ver, exts)
+
+		contact=gajim.contacts.get_contact_from_full_jid(self, jid)
+		if contact is None:
+			return	# TODO: a way to put contact not-in-roster into Contacts
+
+		# overwriting old data
+		contact.caps_node=node
+		contact.caps_ver=ver
+		contact.caps_exts=exts
diff --git a/src/common/contacts.py b/src/common/contacts.py
index 06f8e2ecae..3db62e9cae 100644
--- a/src/common/contacts.py
+++ b/src/common/contacts.py
@@ -33,6 +33,12 @@ class Contact:
 		self.priority = priority
 		self.keyID = keyID
 
+		# 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
+
 		# please read jep-85 http://www.jabber.org/jeps/jep-0085.html
 		# we keep track of jep85 support with the peer by three extra states:
 		# None, False and 'ask'
-- 
GitLab