diff --git a/src/common/caps.py b/src/common/caps.py new file mode 100644 index 000000000..e052fc119 --- /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 06f8e2eca..3db62e9ca 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'