Split common/caps.py. We now have common/protocol/ to contain XMPP connection related classes.

Plan is to move our ConnectionX classess to the protocol package one by one. Each move should be more than a simple copy paste. It should be preceeded by cleanups and the like.
This commit is contained in:
Stephan Erb 2009-12-10 20:06:46 +01:00
parent 7708e3b87e
commit e9caf06992
13 changed files with 233 additions and 179 deletions

View File

@ -1,5 +1,5 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
## src/common/caps.py ## src/common/caps_cache.py
## ##
## Copyright (C) 2007 Tomasz Melcer <liori AT exroot.org> ## Copyright (C) 2007 Tomasz Melcer <liori AT exroot.org>
## Travis Shirk <travis AT pobox.com> ## Travis Shirk <travis AT pobox.com>
@ -31,13 +31,11 @@ CapsCache caches features to hash relationships. The cache is queried
through ClientCaps objects which are hold by contact instances. through ClientCaps objects which are hold by contact instances.
""" """
import gajim
import helpers
import base64 import base64
import hashlib import hashlib
import logging import logging
log = logging.getLogger('gajim.c.caps') log = logging.getLogger('gajim.c.caps_cache')
from common.xmpp import (NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION, NS_CHATSTATES, from common.xmpp import (NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION, NS_CHATSTATES,
NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO, NS_CAPS) NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO, NS_CAPS)
@ -75,6 +73,20 @@ def client_supports(client_caps, requested_feature):
return requested_feature not in FEATURE_BLACKLIST return requested_feature not in FEATURE_BLACKLIST
else: else:
return False return False
def create_suitable_client_caps(node, caps_hash, hash_method):
"""
Create and return a suitable ClientCaps object for the given node,
caps_hash, hash_method combination.
"""
if not node or not caps_hash:
# improper caps, ignore client capabilities.
client_caps = NullClientCaps()
elif not hash_method:
client_caps = OldClientCaps(caps_hash, node)
else:
client_caps = ClientCaps(caps_hash, node, hash_method)
return client_caps
def compute_caps_hash(identities, features, dataforms=[], hash_method='sha-1'): def compute_caps_hash(identities, features, dataforms=[], hash_method='sha-1'):
""" """
@ -151,22 +163,6 @@ def compute_caps_hash(identities, features, dataforms=[], hash_method='sha-1'):
### Internal classes of this module ### Internal classes of this module
################################################################################ ################################################################################
def create_suitable_client_caps(node, caps_hash, hash_method):
"""
Create and return a suitable ClientCaps object for the given node,
caps_hash, hash_method combination.
"""
if not node or not caps_hash:
# improper caps, ignore client capabilities.
client_caps = NullClientCaps()
elif not hash_method:
client_caps = OldClientCaps(caps_hash, node)
else:
client_caps = ClientCaps(caps_hash, node, hash_method)
return client_caps
class AbstractClientCaps(object): class AbstractClientCaps(object):
""" """
Base class representing a client and its capabilities as advertised by a Base class representing a client and its capabilities as advertised by a
@ -208,7 +204,6 @@ class ClientCaps(AbstractClientCaps):
""" """
The current XEP-115 implementation The current XEP-115 implementation
""" """
def __init__(self, caps_hash, node, hash_method): def __init__(self, caps_hash, node, hash_method):
AbstractClientCaps.__init__(self, caps_hash, node) AbstractClientCaps.__init__(self, caps_hash, node)
assert hash_method != 'old' assert hash_method != 'old'
@ -230,7 +225,6 @@ class OldClientCaps(AbstractClientCaps):
""" """
Old XEP-115 implemtation. Kept around for background competability Old XEP-115 implemtation. Kept around for background competability
""" """
def __init__(self, caps_hash, node): def __init__(self, caps_hash, node):
AbstractClientCaps.__init__(self, caps_hash, node) AbstractClientCaps.__init__(self, caps_hash, node)
@ -251,7 +245,6 @@ class NullClientCaps(AbstractClientCaps):
Assumes (almost) everything is supported. Assumes (almost) everything is supported.
""" """
def __init__(self): def __init__(self):
AbstractClientCaps.__init__(self, None, None) AbstractClientCaps.__init__(self, None, None)
@ -273,7 +266,6 @@ class CapsCache(object):
This object keeps the mapping between caps data and real disco features they This object keeps the mapping between caps data and real disco features they
represent, and provides simple way to query that info represent, and provides simple way to query that info
""" """
def __init__(self, logger=None): def __init__(self, logger=None):
# our containers: # our containers:
# __cache is a dictionary mapping: pair of hash method and hash maps # __cache is a dictionary mapping: pair of hash method and hash maps
@ -345,6 +337,13 @@ class CapsCache(object):
if not self._recently_seen: if not self._recently_seen:
self._recently_seen = True self._recently_seen = True
self._logger.update_caps_time(self.hash_method, self.hash) self._logger.update_caps_time(self.hash_method, self.hash)
def is_valid(self):
"""
Returns True if identities and features for this cache item
are known.
"""
return self.status == CACHED
self.__CacheItem = CacheItem self.__CacheItem = CacheItem
self.logger = logger self.logger = logger
@ -391,80 +390,4 @@ class CapsCache(object):
else: else:
q.update_last_seen() q.update_last_seen()
################################################################################ # vim: se ts=3:
### Caps network coding
################################################################################
class ConnectionCaps(object):
def __init__(self, account, dispatch_event):
self._account = account
self._dispatch_event = dispatch_event
def _capsPresenceCB(self, con, presence):
"""
XMMPPY callback method to handle retrieved caps info
"""
try:
jid = helpers.get_full_jid_from_iq(presence)
except:
log.info("Ignoring invalid JID in caps presenceCB")
return
client_caps = self._extract_client_caps_from_presence(presence)
capscache.query_client_of_jid_if_unknown(self, jid, client_caps)
self._update_client_caps_of_contact(jid, client_caps)
self._dispatch_event('CAPS_RECEIVED', (jid,))
def _extract_client_caps_from_presence(self, presence):
caps_tag = presence.getTag('c', namespace=NS_CAPS)
if caps_tag:
hash_method, node, caps_hash = caps_tag['hash'], caps_tag['node'], caps_tag['ver']
else:
hash_method = node = caps_hash = None
return create_suitable_client_caps(node, caps_hash, hash_method)
def _update_client_caps_of_contact(self, jid, client_caps):
contact = self._get_contact_or_gc_contact_for_jid(jid)
if contact:
contact.client_caps = client_caps
else:
log.info("Received Caps from unknown contact %s" % jid)
def _get_contact_or_gc_contact_for_jid(self, jid):
contact = gajim.contacts.get_contact_from_full_jid(self._account, jid)
if contact is None:
room_jid, nick = gajim.get_room_and_nick_from_fjid(jid)
contact = gajim.contacts.get_gc_contact(self._account, room_jid, nick)
return contact
def _capsDiscoCB(self, jid, node, identities, features, dataforms):
"""
XMMPPY callback to update our caps cache with queried information after
we have retrieved an unknown caps hash and issued a disco
"""
contact = self._get_contact_or_gc_contact_for_jid(jid)
if not contact:
log.info("Received Disco from unknown contact %s" % jid)
return
lookup = contact.client_caps.get_cache_lookup_strategy()
cache_item = lookup(capscache)
if cache_item.status == CACHED:
return
else:
validate = contact.client_caps.get_hash_validation_strategy()
hash_is_valid = validate(identities, features, dataforms)
if hash_is_valid:
cache_item.set_and_store(identities, features)
else:
contact.client_caps = NullClientCaps()
log.warn("Computed and retrieved caps hash differ." +
"Ignoring caps of contact %s" % contact.get_full_jid())
self._dispatch_event('CAPS_RECEIVED', (jid,))
# vim: se ts=3:

View File

@ -50,7 +50,8 @@ from common import exceptions
from common.commands import ConnectionCommands from common.commands import ConnectionCommands
from common.pubsub import ConnectionPubSub from common.pubsub import ConnectionPubSub
from common.pep import ConnectionPEP from common.pep import ConnectionPEP
from common.caps import ConnectionCaps from common.protocol.caps import ConnectionCaps
import common.caps_cache as capscache
if gajim.HAVE_FARSIGHT: if gajim.HAVE_FARSIGHT:
from common.jingle import ConnectionJingle from common.jingle import ConnectionJingle
else: else:
@ -1520,7 +1521,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream,
ConnectionPEP.__init__(self, account=self.name, dispatcher=self, ConnectionPEP.__init__(self, account=self.name, dispatcher=self,
pubsub_connection=self) pubsub_connection=self)
ConnectionCaps.__init__(self, account=self.name, ConnectionCaps.__init__(self, account=self.name,
dispatch_event=self.dispatch) dispatch_event=self.dispatch, capscache=capscache.capscache,
client_caps_factory=capscache.create_suitable_client_caps)
ConnectionJingle.__init__(self) ConnectionJingle.__init__(self)
ConnectionHandlersBase.__init__(self) ConnectionHandlersBase.__init__(self)
self.gmail_url = None self.gmail_url = None

View File

@ -29,7 +29,7 @@
## ##
from common import caps from common import caps_cache
from common.account import Account from common.account import Account
import common.gajim import common.gajim
@ -54,7 +54,7 @@ class CommonContact(XMPPEntity):
self.status = status self.status = status
self.name = name self.name = name
self.client_caps = client_caps or caps.NullClientCaps() self.client_caps = client_caps or caps_cache.NullClientCaps()
# please read xep-85 http://www.xmpp.org/extensions/xep-0085.html # 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: # we keep track of xep85 support with the peer by three extra states:
@ -89,7 +89,7 @@ class CommonContact(XMPPEntity):
# return caps for a contact that has no resources left. # return caps for a contact that has no resources left.
return False return False
else: else:
return caps.client_supports(self.client_caps, requested_feature) return caps_cache.client_supports(self.client_caps, requested_feature)
class Contact(CommonContact): class Contact(CommonContact):

View File

@ -211,8 +211,8 @@ gajim_optional_features = {}
# Capabilities hash per account # Capabilities hash per account
caps_hash = {} caps_hash = {}
import caps import caps_cache
caps.initialize(logger) caps_cache.initialize(logger)
def get_nick_from_jid(jid): def get_nick_from_jid(jid):
pos = jid.find('@') pos = jid.find('@')

View File

@ -38,7 +38,7 @@ import errno
import select import select
import base64 import base64
import hashlib import hashlib
import caps import caps_cache
from encodings.punycode import punycode_encode from encodings.punycode import punycode_encode
@ -1309,7 +1309,7 @@ def update_optional_features(account = None):
gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP_AUDIO) gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP_AUDIO)
gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP_VIDEO) gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP_VIDEO)
gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_ICE_UDP) gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_ICE_UDP)
gajim.caps_hash[a] = caps.compute_caps_hash([gajim.gajim_identity], gajim.caps_hash[a] = caps_cache.compute_caps_hash([gajim.gajim_identity],
gajim.gajim_common_features + gajim.gajim_optional_features[a]) gajim.gajim_common_features + gajim.gajim_optional_features[a])
# re-send presence with new hash # re-send presence with new hash
connected = gajim.connections[a].connected connected = gajim.connections[a].connected

View File

@ -32,7 +32,7 @@ import re
from time import time from time import time
from common import gajim from common import gajim
from common import helpers from common import helpers
from common import caps from common import caps_cache
import sqlite3 as sqlite import sqlite3 as sqlite
import logger import logger
@ -218,7 +218,7 @@ class OptionsParser:
gajim.logger.init_vars() gajim.logger.init_vars()
gajim.config.set('version', new_version) gajim.config.set('version', new_version)
caps.capscache.initialize_from_db() caps_cache.capscache.initialize_from_db()
def assert_unread_msgs_table_exists(self): def assert_unread_msgs_table_exists(self):
""" """

View File

@ -0,0 +1,3 @@
"""
Implementations of specific XMPP protocols and XEPs
"""

110
src/common/protocol/caps.py Normal file
View File

@ -0,0 +1,110 @@
# -*- coding:utf-8 -*-
## src/common/protocol/caps.py
##
## Copyright (C) 2009 Stephan Erb <steve-e AT h3c.de>
##
## This file is part of Gajim.
##
## Gajim 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 3 only.
##
## Gajim 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.
##
## You should have received a copy of the GNU General Public License
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
##
"""
Module containing the network portion of XEP-115 (Entity Capabilities)
"""
import logging
log = logging.getLogger('gajim.c.p.caps')
from common.xmpp import NS_CAPS
from common import gajim
from common import helpers
class ConnectionCaps(object):
def __init__(self, account, dispatch_event, capscache, client_caps_factory):
self._account = account
self._dispatch_event = dispatch_event
self._capscache = capscache
self._create_suitable_client_caps = client_caps_factory
def _capsPresenceCB(self, con, presence):
"""
XMMPPY callback method to handle retrieved caps info
"""
try:
jid = helpers.get_full_jid_from_iq(presence)
except:
log.info("Ignoring invalid JID in caps presenceCB")
return
client_caps = self._extract_client_caps_from_presence(presence)
self._capscache.query_client_of_jid_if_unknown(self, jid, client_caps)
self._update_client_caps_of_contact(jid, client_caps)
self._dispatch_event('CAPS_RECEIVED', (jid,))
def _extract_client_caps_from_presence(self, presence):
caps_tag = presence.getTag('c', namespace=NS_CAPS)
if caps_tag:
hash_method, node, caps_hash = caps_tag['hash'], caps_tag['node'], caps_tag['ver']
else:
hash_method = node = caps_hash = None
return self._create_suitable_client_caps(node, caps_hash, hash_method)
def _update_client_caps_of_contact(self, jid, client_caps):
contact = self._get_contact_or_gc_contact_for_jid(jid)
if contact:
contact.client_caps = client_caps
else:
log.info("Received Caps from unknown contact %s" % jid)
def _get_contact_or_gc_contact_for_jid(self, jid):
contact = gajim.contacts.get_contact_from_full_jid(self._account, jid)
if contact is None:
room_jid, nick = gajim.get_room_and_nick_from_fjid(jid)
contact = gajim.contacts.get_gc_contact(self._account, room_jid, nick)
return contact
def _capsDiscoCB(self, jid, node, identities, features, dataforms):
"""
XMMPPY callback to update our caps cache with queried information after
we have retrieved an unknown caps hash and issued a disco
"""
contact = self._get_contact_or_gc_contact_for_jid(jid)
if not contact:
log.info("Received Disco from unknown contact %s" % jid)
return
lookup = contact.client_caps.get_cache_lookup_strategy()
cache_item = lookup(self._capscache)
if cache_item.is_valid():
return
else:
validate = contact.client_caps.get_hash_validation_strategy()
hash_is_valid = validate(identities, features, dataforms)
if hash_is_valid:
cache_item.set_and_store(identities, features)
else:
node = caps_hash = hash_method = None
contact.client_caps = self._create_suitable_client_caps(node,
caps_hash, hash_method)
log.warn("Computed and retrieved caps hash differ." +
"Ignoring caps of contact %s" % contact.get_full_jid())
self._dispatch_event('CAPS_RECEIVED', (jid,))
# vim: se ts=3:

View File

@ -71,7 +71,7 @@ import common.sleepy
from common.xmpp import idlequeue from common.xmpp import idlequeue
from common.zeroconf import connection_zeroconf from common.zeroconf import connection_zeroconf
from common import resolver from common import resolver
from common import caps from common import caps_cache
from common import proxy65_manager from common import proxy65_manager
from common import socks5 from common import socks5
from common import helpers from common import helpers
@ -3307,7 +3307,7 @@ class Interface:
helpers.update_optional_features() helpers.update_optional_features()
# prepopulate data which we are sure of; note: we do not log these info # prepopulate data which we are sure of; note: we do not log these info
for account in gajim.connections: for account in gajim.connections:
gajimcaps = caps.capscache[('sha-1', gajim.caps_hash[account])] gajimcaps = caps_cache.capscache[('sha-1', gajim.caps_hash[account])]
gajimcaps.identities = [gajim.gajim_identity] gajimcaps.identities = [gajim.gajim_identity]
gajimcaps.features = gajim.gajim_common_features + \ gajimcaps.features = gajim.gajim_common_features + \
gajim.gajim_optional_features[account] gajim.gajim_optional_features[account]

View File

@ -37,7 +37,8 @@ for o, a in opts:
# new test modules need to be added manually # new test modules need to be added manually
modules = ( 'unit.test_xmpp_dispatcher_nb', modules = ( 'unit.test_xmpp_dispatcher_nb',
'unit.test_xmpp_transports_nb', 'unit.test_xmpp_transports_nb',
'unit.test_caps', 'unit.test_protocol_caps',
'unit.test_caps_cache',
'unit.test_contacts', 'unit.test_contacts',
'unit.test_sessions', 'unit.test_sessions',
'unit.test_account', 'unit.test_account',

View File

@ -1,14 +1,13 @@
''' '''
Tests for capabilities and the capabilities cache Tests for capabilities and the capabilities cache
''' '''
from common.caps import NullClientCaps
import unittest import unittest
import lib import lib
lib.setup_env() lib.setup_env()
from common.xmpp import NS_MUC, NS_PING, NS_XHTML_IM from common.xmpp import NS_MUC, NS_PING, NS_XHTML_IM
from common import caps from common import caps_cache as caps
from common.contacts import Contact from common.contacts import Contact
from mock import Mock from mock import Mock
@ -146,65 +145,6 @@ class TestOldClientCaps(TestClientCaps):
connection.mockCheckCall(0, "discoverInfo", "test@gajim.org") connection.mockCheckCall(0, "discoverInfo", "test@gajim.org")
from common.xmpp import simplexml
from common.xmpp import protocol
class TestableConnectionCaps(caps.ConnectionCaps):
def __init__(self, *args, **kwargs):
self._mocked_contacts = {}
caps.ConnectionCaps.__init__(self, *args, **kwargs)
def _get_contact_or_gc_contact_for_jid(self, jid):
"""
Overwrite to decouple form contact handling
"""
if jid not in self._mocked_contacts:
self._mocked_contacts[jid] = Mock(realClass=Contact)
self._mocked_contacts[jid].jid = jid
return self._mocked_contacts[jid]
def discoverInfo(self, *args, **kwargs):
pass
def get_mocked_contact_for_jid(self, jid):
return self._mocked_contacts[jid]
class TestConnectionCaps(CommonCapsTest):
def test_capsPresenceCB(self):
jid = "user@server.com/a"
connection_caps = TestableConnectionCaps("account",
self._build_assertering_dispatcher_function("CAPS_RECEIVED", jid))
xml = """<presence from='user@server.com/a'
to='%s' id='123'>
<c node='http://gajim.org' ver='pRCD6cgQ4SDqNMCjdhRV6TECx5o='
hash='sha-1' xmlns='http://jabber.org/protocol/caps'/>
</presence>
""" % (jid)
iq = protocol.Iq(node=simplexml.XML2Node(xml))
connection_caps._capsPresenceCB(None, iq)
self.assertTrue(self._dispatcher_called, msg="Must have received caps")
client_caps = connection_caps.get_mocked_contact_for_jid(jid).client_caps
self.assertTrue(client_caps, msg="Client caps must be set")
self.assertFalse(isinstance(client_caps, NullClientCaps),
msg="On receive of proper caps, we must not use the fallback")
def _build_assertering_dispatcher_function(self, expected_event, jid):
self._dispatcher_called = False
def dispatch(event, data):
self.assertFalse(self._dispatcher_called, msg="Must only be called once")
self._dispatcher_called = True
self.assertEqual(expected_event, event)
self.assertEqual(jid, data[0])
return dispatch
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -9,7 +9,7 @@ lib.setup_env()
from common.contacts import CommonContact, Contact, GC_Contact, LegacyContactsAPI from common.contacts import CommonContact, Contact, GC_Contact, LegacyContactsAPI
from common.xmpp import NS_MUC from common.xmpp import NS_MUC
from common import caps from common import caps_cache
class TestCommonContact(unittest.TestCase): class TestCommonContact(unittest.TestCase):
@ -23,11 +23,11 @@ class TestCommonContact(unittest.TestCase):
Test the caps support method of contacts. Test the caps support method of contacts.
See test_caps for more enhanced tests. See test_caps for more enhanced tests.
''' '''
caps.capscache = caps.CapsCache() caps_cache.capscache = caps_cache.CapsCache()
self.assertTrue(self.contact.supports(NS_MUC), self.assertTrue(self.contact.supports(NS_MUC),
msg="Must not backtrace on simple check for supported feature") msg="Must not backtrace on simple check for supported feature")
self.contact.client_caps = caps.NullClientCaps() self.contact.client_caps = caps_cache.NullClientCaps()
self.assertTrue(self.contact.supports(NS_MUC), self.assertTrue(self.contact.supports(NS_MUC),
msg="Must not backtrace on simple check for supported feature") msg="Must not backtrace on simple check for supported feature")

View File

@ -0,0 +1,75 @@
'''
Tests for caps network coding
'''
import unittest
import lib
lib.setup_env()
from common import caps_cache
from common.protocol import caps
from common.contacts import Contact
from mock import Mock
from common.xmpp import simplexml
from common.xmpp import protocol
class TestableConnectionCaps(caps.ConnectionCaps):
def __init__(self, *args, **kwargs):
self._mocked_contacts = {}
caps.ConnectionCaps.__init__(self, *args, **kwargs)
def _get_contact_or_gc_contact_for_jid(self, jid):
"""
Overwrite to decouple form contact handling
"""
if jid not in self._mocked_contacts:
self._mocked_contacts[jid] = Mock(realClass=Contact)
self._mocked_contacts[jid].jid = jid
return self._mocked_contacts[jid]
def discoverInfo(self, *args, **kwargs):
pass
def get_mocked_contact_for_jid(self, jid):
return self._mocked_contacts[jid]
class TestConnectionCaps(unittest.TestCase):
def test_capsPresenceCB(self):
jid = "user@server.com/a"
connection_caps = TestableConnectionCaps("account",
self._build_assertering_dispatcher_function("CAPS_RECEIVED", jid),
Mock(), caps_cache.create_suitable_client_caps)
xml = """<presence from='user@server.com/a' to='%s' id='123'>
<c node='http://gajim.org' ver='pRCD6cgQ4SDqNMCjdhRV6TECx5o='
hash='sha-1' xmlns='http://jabber.org/protocol/caps'/>
</presence>
""" % (jid)
iq = protocol.Iq(node=simplexml.XML2Node(xml))
connection_caps._capsPresenceCB(None, iq)
self.assertTrue(self._dispatcher_called, msg="Must have received caps")
client_caps = connection_caps.get_mocked_contact_for_jid(jid).client_caps
self.assertTrue(client_caps, msg="Client caps must be set")
self.assertFalse(isinstance(client_caps, caps_cache.NullClientCaps),
msg="On receive of proper caps, we must not use the fallback")
def _build_assertering_dispatcher_function(self, expected_event, jid):
self._dispatcher_called = False
def dispatch(event, data):
self.assertFalse(self._dispatcher_called, msg="Must only be called once")
self._dispatcher_called = True
self.assertEqual(expected_event, event)
self.assertEqual(jid, data[0])
return dispatch
if __name__ == '__main__':
unittest.main()
# vim: se ts=3: