Redistribute responsibility: Let contact instances check if features are supported

This commit is contained in:
Stephan Erb 2009-10-27 20:31:09 +01:00
parent 3295b08b26
commit c7ff97703f
5 changed files with 188 additions and 262 deletions

View File

@ -39,51 +39,30 @@ import gajim
import helpers
from common.xmpp import NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION, NS_CHATSTATES
# Features where we cannot safely assume that the other side supports them
FEATURE_BLACKLIST = [NS_CHATSTATES, NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION]
class AbstractClientCaps(object):
'''
Base class representing a client and its capabilities as advertised by
a caps tag in a presence
'''
def __init__(self, caps_cache, caps_hash, node):
self._caps_cache = caps_cache
def __init__(self, caps_hash, node):
self._hash = caps_hash
self._node = node
def query_client_of_jid_if_unknown(self, connection, jid):
'''
Asynchronously query the give jid for its (node, ver, exts) caps data
using a disco query.
Query will only be sent if the data is not already cached.
'''
q = self._lookup_in_cache()
if q.queried == 0:
self._discover(connection, jid)
q.queried = 1
def contains_feature(self, requested_feature):
''' Returns true if these capabilities contain the given feature '''
cach_entry = self._lookup_in_cache()
supported_features = cach_entry.features
if requested_feature in supported_features:
return True
elif supported_features == [] and cach_entry.queried in (0, 1):
# assume feature is supported, if not blacklisted
return requested_feature not in FEATURE_BLACKLIST
else:
return False
def get_discover_strategy(self):
return self._discover
def _discover(self, connection, jid):
''' To be implemented by subclassess '''
raise NotImplementedError()
def _lookup_in_cache(self):
def get_cache_lookup_strategy(self):
return self._lookup_in_cache
def _lookup_in_cache(self, caps_cache):
''' To be implemented by subclassess '''
raise NotImplementedError()
@ -91,13 +70,13 @@ class AbstractClientCaps(object):
class ClientCaps(AbstractClientCaps):
''' The current XEP-115 implementation '''
def __init__(self, caps_cache, caps_hash, node, hash_method):
AbstractClientCaps.__init__(self, caps_cache, caps_hash, node)
def __init__(self, caps_hash, node, hash_method):
AbstractClientCaps.__init__(self, caps_hash, node)
assert hash_method != 'old'
self._hash_method = hash_method
def _lookup_in_cache(self):
return self._caps_cache[(self._hash_method, self._hash)]
def _lookup_in_cache(self, caps_cache):
return caps_cache[(self._hash_method, self._hash)]
def _discover(self, connection, jid):
connection.discoverInfo(jid, '%s#%s' % (self._node, self._hash))
@ -106,11 +85,11 @@ class ClientCaps(AbstractClientCaps):
class OldClientCaps(AbstractClientCaps):
''' Old XEP-115 implemtation. Kept around for background competability. '''
def __init__(self, caps_cache, caps_hash, node):
AbstractClientCaps.__init__(self, caps_cache, caps_hash, node)
def __init__(self, caps_hash, node):
AbstractClientCaps.__init__(self, caps_hash, node)
def _lookup_in_cache(self):
return self._caps_cache[('old', self._node + '#' + self._hash)]
def _lookup_in_cache(self, caps_cache):
return caps_cache[('old', self._node + '#' + self._hash)]
def _discover(self, connection, jid):
connection.discoverInfo(jid)
@ -125,32 +104,19 @@ class NullClientCaps(AbstractClientCaps):
'''
def __init__(self):
pass
AbstractClientCaps.__init__(self, None, None)
def query_client_of_jid_if_unknown(self, connection, jid):
pass
def _lookup_in_cache(self, caps_cache):
return caps_cache[('old', '')]
def contains_feature(self, feature):
return feature not in FEATURE_BLACKLIST
def _discover(self, connection, jid):
pass
class CapsCache(object):
''' This object keeps the mapping between caps data and real disco
'''
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.
'''
def __init__(self, logger=None):
''' Create a cache for entity capabilities. '''
@ -219,15 +185,14 @@ class CapsCache(object):
def update(self, identities, features):
# NOTE: self refers to CapsCache object, not to CacheItem
self.identities=identities
self.features=features
self.identities = identities
self.features = features
self._logger.add_caps_entry(self.hash_method, self.hash,
identities, features)
self.__CacheItem = CacheItem
# prepopulate data which we are sure of; note: we do not log these info
for account in gajim.connections:
gajimcaps = self[('sha-1', gajim.caps_hash[account])]
gajimcaps.identities = [gajim.gajim_identity]
@ -257,48 +222,19 @@ class CapsCache(object):
self.__cache[(hash_method, hash_)] = x
return x
def preload(self, con, jid, node, hash_method, hash_):
def query_client_of_jid_if_unknown(self, connection, jid, client_caps):
''' 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. '''
if hash_method == 'old':
q = self[(hash_method, node + '#' + hash_)]
else:
q = self[(hash_method, hash_)]
if q.queried==0:
lookup_cache_item = client_caps.get_cache_lookup_strategy()
q = lookup_cache_item(self)
if q.queried == 0:
# do query for bare node+hash pair
# this will create proper object
q.queried=1
if hash_method == 'old':
con.discoverInfo(jid)
else:
con.discoverInfo(jid, '%s#%s' % (node, hash_))
def is_supported(self, contact, feature):
if not contact:
return False
# Unfortunately, if all resources are offline, the contact
# includes the last resource that was online. Check for its
# show, so we can be sure it's existant. Otherwise, we still
# return caps for a contact that has no resources left.
if contact.show == 'offline':
return False
# FIXME: We assume everything is supported if we got no caps.
# This is the "Asterix way", after 0.12 release, I will
# likely implement a fallback to disco (could be disabled
# for mobile users who pay for traffic)
if contact.caps_hash_method == 'old':
features = self[(contact.caps_hash_method, contact.caps_node + '#' + \
contact.caps_hash)].features
else:
features = self[(contact.caps_hash_method, contact.caps_hash)].features
if feature in features or features == []:
return True
return False
q.queried = 1
discover = client_caps.get_discover_strategy()
discover(connection, jid)
gajim.capscache = CapsCache(gajim.logger)

View File

@ -30,12 +30,15 @@
import common.gajim
from common.caps import NullClientCaps, FEATURE_BLACKLIST
class Contact(object):
'''Information concerning each contact'''
def __init__(self, jid='', name='', groups=[], show='', status='', sub='',
ask='', resource='', priority=0, keyID='', caps_node=None,
caps_hash_method=None, caps_hash=None, our_chatstate=None, chatstate=None,
last_status_time=None, msg_id = None, composing_xep = None, mood={}, tune={},
ask='', resource='', priority=0, keyID='', client_caps=None, caps_cache=None,
our_chatstate=None, chatstate=None, last_status_time=None, msg_id = None,
composing_xep = None, mood={}, tune={},
activity={}):
self.jid = jid
self.name = name
@ -49,11 +52,9 @@ class Contact(object):
self.priority = priority
self.keyID = keyID
# Capabilities; filled by caps.py/ConnectionCaps object
# every time it gets these from presence stanzas
self.caps_node = caps_node
self.caps_hash_method = caps_hash_method
self.caps_hash = caps_hash
# Entity Capabilities
self._client_caps = client_caps or NullClientCaps()
self._caps_cache = caps_cache or common.gajim.capscache
# 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:
@ -135,32 +136,34 @@ class Contact(object):
return False
def _set_supported_caps(self, value):
'''
Set an EntityCapabilities object
'''
self._caps = value
def _get_supported_caps(self):
'''
Returns a function which delegates to the EntityCapabilites support checker
This allows easy checks like:
if contact.supports(NS_COOL_FEATURE): ...
'''
def test(feature):
if self.show == 'offline':
# Unfortunately, if all resources are offline, the contact
# includes the last resource that was online. Check for its
# show, so we can be sure it's existant. Otherwise, we still
# return caps for a contact that has no resources left.
return False
else:
return self._caps.contains_feature(feature)
return test
supports = property(_get_supported_caps, _set_supported_caps)
def supports(self, requested_feature):
if self.show == 'offline':
# Unfortunately, if all resources are offline, the contact
# includes the last resource that was online. Check for its
# show, so we can be sure it's existant. Otherwise, we still
# return caps for a contact that has no resources left.
return False
else:
return self._client_supports(requested_feature)
def _client_supports(self, requested_feature):
lookup_item = self._client_caps.get_cache_lookup_strategy()
cache_item = lookup_item(self._caps_cache)
supported_features = cache_item.features
if requested_feature in supported_features:
return True
elif supported_features == [] and cache_item.queried in (0, 1):
# assume feature is supported, if we don't know yet, what the client
# is capable of
return requested_feature not in FEATURE_BLACKLIST
else:
return False
def set_supported_client_caps(self, client_caps):
''' Set an EntityCapabilities object '''
self._client_caps = client_caps
class GC_Contact:
'''Information concerning each groupchat contact'''
@ -175,9 +178,11 @@ class GC_Contact:
self.affiliation = affiliation
self.jid = jid
self.resource = resource
self.caps_node = None
self.caps_hash_method = None
self.caps_hash = None
# Entity Capabilities
self._client_caps = NullClientCaps()
self._caps_cache = common.gajim.capscache
self.our_chatstate = our_chatstate
self.composing_xep = composing_xep
self.chatstate = chatstate
@ -188,31 +193,33 @@ class GC_Contact:
def get_shown_name(self):
return self.name
def _set_supported_caps(self, value):
'''
Set an EntityCapabilities object
'''
self._caps = value
def _get_supported_caps(self):
'''
Returns a function which delegates to the EntityCapabilites support checker
This allows easy checks like:
if contact.supports(NS_COOL_FEATURE): ...
'''
def test(feature):
if self.show == 'offline':
# Unfortunately, if all resources are offline, the contact
# includes the last resource that was online. Check for its
# show, so we can be sure it's existant. Otherwise, we still
# return caps for a contact that has no resources left.
return False
else:
return self._caps.contains_feature(feature)
return test
supports = property(_get_supported_caps, _set_supported_caps)
def supports(self, requested_feature):
if self.show == 'offline':
# Unfortunately, if all resources are offline, the contact
# includes the last resource that was online. Check for its
# show, so we can be sure it's existant. Otherwise, we still
# return caps for a contact that has no resources left.
return False
else:
return self._client_supports(requested_feature)
def _client_supports(self, requested_feature):
lookup_item = self._client_caps.get_cache_lookup_strategy()
cache_item = lookup_item(self._caps_cache)
supported_features = cache_item.features
if requested_feature in supported_features:
return True
elif supported_features == [] and cache_item.queried in (0, 1):
# assume feature is supported, if we don't know yet, what the client
# is capable of
return requested_feature not in FEATURE_BLACKLIST
else:
return False
def set_supported_client_caps(self, client_caps):
''' Set an EntityCapabilities object '''
self._client_caps = client_caps
class Contacts:
'''Information concerning all contacts and groupchat contacts'''
@ -246,8 +253,8 @@ class Contacts:
del self._metacontacts_tags[account]
def create_contact(self, jid='', name='', groups=[], show='', status='',
sub='', ask='', resource='', priority=0, keyID='', caps_node=None,
caps_hash_method=None, caps_hash=None, our_chatstate=None,
sub='', ask='', resource='', priority=0, keyID='', client_caps=None,
caps_cache=None, our_chatstate=None,
chatstate=None, last_status_time=None, composing_xep=None,
mood={}, tune={}, activity={}):
@ -259,8 +266,8 @@ class Contacts:
return Contact(jid=jid, name=name, groups=groups_unique, show=show,
status=status, sub=sub, ask=ask, resource=resource, priority=priority,
keyID=keyID, caps_node=caps_node, caps_hash_method=caps_hash_method,
caps_hash=caps_hash, our_chatstate=our_chatstate, chatstate=chatstate,
keyID=keyID, client_caps=client_caps, caps_cache=caps_cache,
our_chatstate=our_chatstate, chatstate=chatstate,
last_status_time=last_status_time, composing_xep=composing_xep,
mood=mood, tune=tune, activity=activity)
@ -269,8 +276,8 @@ class Contacts:
groups=contact.groups, show=contact.show, status=contact.status,
sub=contact.sub, ask=contact.ask, resource=contact.resource,
priority=contact.priority, keyID=contact.keyID,
caps_node=contact.caps_node, caps_hash_method=contact.caps_hash_method,
caps_hash=contact.caps_hash, our_chatstate=contact.our_chatstate,
client_caps=contact._client_caps, caps_cache=contact._caps_cache,
our_chatstate=contact.our_chatstate,
chatstate=contact.chatstate, last_status_time=contact.last_status_time)
def add_contact(self, account, contact):
@ -640,9 +647,8 @@ class Contacts:
jid = gc_contact.get_full_jid()
return Contact(jid=jid, resource=gc_contact.resource,
name=gc_contact.name, groups=[], show=gc_contact.show,
status=gc_contact.status, sub='none', caps_node=gc_contact.caps_node,
caps_hash_method=gc_contact.caps_hash_method,
caps_hash=gc_contact.caps_hash)
status=gc_contact.status, sub='none', client_caps=gc_contact._client_caps,
caps_cache=gc_contact._caps_cache)
def create_gc_contact(self, room_jid='', name='', show='', status='',
role='', affiliation='', jid='', resource=''):

View File

@ -32,8 +32,6 @@ import logging
import locale
import config
from contacts import Contacts
from events import Events
import xmpp
try:
@ -101,6 +99,9 @@ else:
os_info = None # used to cache os information
from contacts import Contacts
from events import Events
gmail_domains = ['gmail.com', 'googlemail.com']
transport_type = {} # list the type of transport

View File

@ -6,11 +6,11 @@ import unittest
import lib
lib.setup_env()
from common import gajim
from common import helpers
from common.contacts import Contact
from common.xmpp import NS_MUC, NS_PING, NS_XHTML_IM
from common.caps import CapsCache, ClientCaps, OldClientCaps
from common.contacts import Contact
from mock import Mock
@ -20,7 +20,7 @@ class CommonCapsTest(unittest.TestCase):
def setUp(self):
self.caps_method = 'sha-1'
self.caps_hash = 'm3P2WeXPMGVH2tZPe7yITnfY0Dw='
self.caps = (self.caps_method, self.caps_hash)
self.client_caps = (self.caps_method, self.caps_hash)
self.node = "http://gajim.org"
self.identity = {'category': 'client', 'type': 'pc', 'name':'Gajim'}
@ -35,6 +35,10 @@ class CommonCapsTest(unittest.TestCase):
self.logger = Mock(returnValues={"iter_caps_data":db_caps_cache})
self.cc = CapsCache(self.logger)
# This is a temporary hack required by the way contacts rely on the
# existance of a cache. Hopefully this can be refactored to work via
# dependency injection
gajim.capscache = self.cc
class TestCapsCache(CommonCapsTest):
@ -42,13 +46,13 @@ class TestCapsCache(CommonCapsTest):
def test_set_retrieve(self):
''' Test basic set / retrieve cycle '''
self.cc[self.caps].identities = self.identities
self.cc[self.caps].features = self.features
self.cc[self.client_caps].identities = self.identities
self.cc[self.client_caps].features = self.features
self.assert_(NS_MUC in self.cc[self.caps].features)
self.assert_(NS_PING not in self.cc[self.caps].features)
self.assert_(NS_MUC in self.cc[self.client_caps].features)
self.assert_(NS_PING not in self.cc[self.client_caps].features)
identities = self.cc[self.caps].identities
identities = self.cc[self.client_caps].identities
self.assertEqual(1, len(identities))
@ -57,9 +61,9 @@ class TestCapsCache(CommonCapsTest):
self.assertEqual('pc', identity['type'])
def test_update(self):
''' Test caps update gets logged into db '''
''' Test client_caps update gets logged into db '''
item = self.cc[self.caps]
item = self.cc[self.client_caps]
item.update(self.identities, self.features)
self.logger.mockCheckCall(0, "add_caps_entry", self.caps_method,
@ -67,45 +71,30 @@ class TestCapsCache(CommonCapsTest):
def test_initialize_from_db(self):
''' Read cashed dummy data from db '''
self.assertEqual(self.cc[self.caps].queried, 0)
self.assertEqual(self.cc[self.client_caps].queried, 0)
self.cc.initialize_from_db()
self.assertEqual(self.cc[self.caps].queried, 2)
self.assertEqual(self.cc[self.client_caps].queried, 2)
def test_preload_triggering_query(self):
''' Make sure that preload issues a disco '''
connection = Mock()
client_caps = ClientCaps(self.caps_hash, self.node, self.caps_method)
self.cc.query_client_of_jid_if_unknown(connection, "test@gajim.org",
client_caps)
self.cc.preload(connection, "test@gajim.org", self.node,
self.caps_method, self.caps_hash)
connection.mockCheckCall(0, "discoverInfo", "test@gajim.org",
"http://gajim.org#m3P2WeXPMGVH2tZPe7yITnfY0Dw=")
self.assertEqual(1, len(connection.mockGetAllCalls()))
def test_no_preload_query_if_cashed(self):
''' Preload must not send a query if the data is already cached '''
connection = Mock()
client_caps = ClientCaps(self.caps_hash, self.node, self.caps_method)
self.cc.initialize_from_db()
self.cc.preload(connection, "test@gajim.org", self.node,
self.caps_method, self.caps_hash)
self.cc.query_client_of_jid_if_unknown(connection, "test@gajim.org",
client_caps)
self.assertEqual(0, len(connection.mockGetAllCalls()))
def test_is_supported(self):
contact = Contact(caps_node=self.node,
caps_hash_method=self.caps_method,
caps_hash=self.caps_hash)
self.assertTrue(self.cc.is_supported(contact, NS_PING),
msg="Assume everything is supported, if we don't have caps")
self.cc.initialize_from_db()
self.assertFalse(self.cc.is_supported(contact, NS_PING),
msg="Must return false on unsupported feature")
self.assertTrue(self.cc.is_supported(contact, NS_MUC),
msg="Must return True on supported feature")
def test_hash(self):
'''tests the hash computation'''
@ -117,41 +106,36 @@ class TestClientCaps(CommonCapsTest):
def setUp(self):
CommonCapsTest.setUp(self)
self.caps = ClientCaps(self.cc, self.caps_hash, self.node,
self.caps_method)
def test_no_query_client_of_jid(self):
''' Client must not be queried if the data is already cached '''
connection = Mock()
self.cc.initialize_from_db()
self.caps.query_client_of_jid_if_unknown(connection, "test@gajim.org")
self.client_caps = ClientCaps(self.caps_hash, self.node, self.caps_method)
self.assertEqual(0, len(connection.mockGetAllCalls()))
def test_query_client_of_jid_if_unknown(self):
def test_query_by_get_discover_strategy(self):
''' Client must be queried if the data is unkown '''
connection = Mock()
self.caps.query_client_of_jid_if_unknown(connection, "test@gajim.org")
discover = self.client_caps.get_discover_strategy()
discover(connection, "test@gajim.org")
connection.mockCheckCall(0, "discoverInfo", "test@gajim.org",
"http://gajim.org#m3P2WeXPMGVH2tZPe7yITnfY0Dw=")
def test_is_supported(self):
self.assertTrue(self.caps.contains_feature(NS_PING),
def test_client_supports(self):
contact = Contact(caps_cache=self.cc)
contact.set_supported_client_caps(self.client_caps)
self.assertTrue(contact.supports(NS_PING),
msg="Assume supported, if we don't have caps")
self.assertFalse(self.caps.contains_feature(NS_XHTML_IM),
self.assertFalse(contact.supports(NS_XHTML_IM),
msg="Must not assume blacklisted feature is supported on default")
self.cc.initialize_from_db()
self.assertFalse(self.caps.contains_feature(NS_PING),
self.assertFalse(contact.supports(NS_PING),
msg="Must return false on unsupported feature")
self.assertTrue(self.caps.contains_feature(NS_XHTML_IM),
self.assertTrue(contact.supports(NS_XHTML_IM),
msg="Must return True on supported feature")
self.assertTrue(self.caps.contains_feature(NS_MUC),
self.assertTrue(contact.supports(NS_MUC),
msg="Must return True on supported feature")
@ -159,25 +143,17 @@ class TestOldClientCaps(TestClientCaps):
def setUp(self):
TestClientCaps.setUp(self)
self.caps = OldClientCaps(self.cc, self.caps_hash, self.node)
def test_no_query_client_of_jid(self):
''' Client must not be queried if the data is already cached '''
connection = Mock()
self.cc.initialize_from_db()
self.caps.query_client_of_jid_if_unknown(connection, "test@gajim.org")
self.assertEqual(0, len(connection.mockGetAllCalls()))
self.client_caps = OldClientCaps(self.caps_hash, self.node)
def test_query_client_of_jid_if_unknown(self):
''' Client must be queried if the data is unkown '''
def test_query_by_get_discover_strategy(self):
''' Client must be queried if the data is unknown '''
connection = Mock()
self.caps.query_client_of_jid_if_unknown(connection, "test@gajim.org")
discover = self.client_caps.get_discover_strategy()
discover(connection, "test@gajim.org")
connection.mockCheckCall(0, "discoverInfo", "test@gajim.org")
if __name__ == '__main__':
unittest.main()

View File

@ -6,36 +6,43 @@ import unittest
import lib
lib.setup_env()
from common.contacts import Contact
from common.contacts import Contact, GC_Contact
from common.caps import NullClientCaps
from common.xmpp import NS_MUC
from mock import Mock
class TestCommonContact(unittest.TestCase):
def setUp(self):
self.contact = Contact()
class TestContact(unittest.TestCase):
def test_default_client_supports(self):
'''
Test the caps support method of contacts.
See test_caps for more enhanced tests.
'''
self.assertTrue(self.contact.supports(NS_MUC),
msg="Must not backtrace on simple check for supported feature")
client_caps = NullClientCaps()
self.contact.set_supported_client_caps(client_caps)
self.assertTrue(self.contact.supports(NS_MUC),
msg="Must not backtrace on simple check for supported feature")
class TestContact(TestCommonContact):
def setUp(self):
TestCommonContact.setUp(self)
self.contact = Contact()
def test_supports(self):
''' Test the Entity Capabilities part of the contact instance '''
NS_MUC = 'http://jabber.org/protocol/muc'
# Test with mocks to get basic set/get property behaviour checked
all_supported_mock_entity_caps = Mock(
returnValues={"contains_feature": True})
nothing_supported_mock_entity_caps = Mock(
returnValues={"contains_feature": False})
contact = Contact()
contact.supports = all_supported_mock_entity_caps
self.assertTrue(contact.supports(NS_MUC))
contact.supports = nothing_supported_mock_entity_caps
self.assertFalse(contact.supports(NS_MUC))
# Test with EntityCapabilites to detect API changes
contact.supports = NullClientCaps()
self.assertTrue(contact.supports(NS_MUC),
msg="Default behaviour is to support everything on unknown caps")
class TestGC_Contact(TestCommonContact):
def setUp(self):
TestCommonContact.setUp(self)
self.contact = GC_Contact()
if __name__ == "__main__":