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 -*-
## src/common/caps.py
## src/common/caps_cache.py
##
## Copyright (C) 2007 Tomasz Melcer <liori AT exroot.org>
## 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.
"""
import gajim
import helpers
import base64
import hashlib
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,
NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO, NS_CAPS)
@ -76,6 +74,20 @@ def client_supports(client_caps, requested_feature):
else:
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'):
"""
Compute caps hash according to XEP-0115, V1.5
@ -151,22 +163,6 @@ def compute_caps_hash(identities, features, dataforms=[], hash_method='sha-1'):
### 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):
"""
Base class representing a client and its capabilities as advertised by a
@ -208,7 +204,6 @@ class ClientCaps(AbstractClientCaps):
"""
The current XEP-115 implementation
"""
def __init__(self, caps_hash, node, hash_method):
AbstractClientCaps.__init__(self, caps_hash, node)
assert hash_method != 'old'
@ -230,7 +225,6 @@ class OldClientCaps(AbstractClientCaps):
"""
Old XEP-115 implemtation. Kept around for background competability
"""
def __init__(self, caps_hash, node):
AbstractClientCaps.__init__(self, caps_hash, node)
@ -251,7 +245,6 @@ class NullClientCaps(AbstractClientCaps):
Assumes (almost) everything is supported.
"""
def __init__(self):
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
represent, and provides simple way to query that info
"""
def __init__(self, logger=None):
# our containers:
# __cache is a dictionary mapping: pair of hash method and hash maps
@ -346,6 +338,13 @@ class CapsCache(object):
self._recently_seen = True
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.logger = logger
@ -391,80 +390,4 @@ class CapsCache(object):
else:
q.update_last_seen()
################################################################################
### 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.pubsub import ConnectionPubSub
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:
from common.jingle import ConnectionJingle
else:
@ -1520,7 +1521,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream,
ConnectionPEP.__init__(self, account=self.name, dispatcher=self,
pubsub_connection=self)
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)
ConnectionHandlersBase.__init__(self)
self.gmail_url = None

View File

@ -29,7 +29,7 @@
##
from common import caps
from common import caps_cache
from common.account import Account
import common.gajim
@ -54,7 +54,7 @@ class CommonContact(XMPPEntity):
self.status = status
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
# 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 False
else:
return caps.client_supports(self.client_caps, requested_feature)
return caps_cache.client_supports(self.client_caps, requested_feature)
class Contact(CommonContact):

View File

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

View File

@ -38,7 +38,7 @@ import errno
import select
import base64
import hashlib
import caps
import caps_cache
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_VIDEO)
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])
# re-send presence with new hash
connected = gajim.connections[a].connected

View File

@ -32,7 +32,7 @@ import re
from time import time
from common import gajim
from common import helpers
from common import caps
from common import caps_cache
import sqlite3 as sqlite
import logger
@ -218,7 +218,7 @@ class OptionsParser:
gajim.logger.init_vars()
gajim.config.set('version', new_version)
caps.capscache.initialize_from_db()
caps_cache.capscache.initialize_from_db()
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.zeroconf import connection_zeroconf
from common import resolver
from common import caps
from common import caps_cache
from common import proxy65_manager
from common import socks5
from common import helpers
@ -3307,7 +3307,7 @@ class Interface:
helpers.update_optional_features()
# prepopulate data which we are sure of; note: we do not log these info
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.features = gajim.gajim_common_features + \
gajim.gajim_optional_features[account]

View File

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

View File

@ -1,14 +1,13 @@
'''
Tests for capabilities and the capabilities cache
'''
from common.caps import NullClientCaps
import unittest
import lib
lib.setup_env()
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 mock import Mock
@ -146,65 +145,6 @@ class TestOldClientCaps(TestClientCaps):
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__':
unittest.main()

View File

@ -9,7 +9,7 @@ lib.setup_env()
from common.contacts import CommonContact, Contact, GC_Contact, LegacyContactsAPI
from common.xmpp import NS_MUC
from common import caps
from common import caps_cache
class TestCommonContact(unittest.TestCase):
@ -23,11 +23,11 @@ class TestCommonContact(unittest.TestCase):
Test the caps support method of contacts.
See test_caps for more enhanced tests.
'''
caps.capscache = caps.CapsCache()
caps_cache.capscache = caps_cache.CapsCache()
self.assertTrue(self.contact.supports(NS_MUC),
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),
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: