Merge refactoring branch back to default
This commit is contained in:
commit
07d0811439
18 changed files with 563 additions and 382 deletions
|
@ -1,4 +1,5 @@
|
|||
syntax: glob
|
||||
*.orig
|
||||
*.gmo
|
||||
*.in
|
||||
*.la
|
||||
|
|
|
@ -1356,9 +1356,7 @@ class ChatControl(ChatControlBase):
|
|||
|
||||
def update_toolbar(self):
|
||||
# Formatting
|
||||
if gajim.capscache.is_supported(self.contact, NS_XHTML_IM) \
|
||||
and not gajim.capscache.is_supported(self.contact, 'notexistant') \
|
||||
and not self.gpg_is_active:
|
||||
if self.contact.supports(NS_XHTML_IM) and not self.gpg_is_active:
|
||||
self._formattings_button.set_sensitive(True)
|
||||
else:
|
||||
self._formattings_button.set_sensitive(False)
|
||||
|
@ -1371,15 +1369,15 @@ class ChatControl(ChatControlBase):
|
|||
self._add_to_roster_button.hide()
|
||||
|
||||
# Jingle detection
|
||||
if gajim.capscache.is_supported(self.contact, NS_JINGLE_ICE_UDP) and \
|
||||
if self.contact.supports(NS_JINGLE_ICE_UDP) and \
|
||||
gajim.HAVE_FARSIGHT and self.contact.resource:
|
||||
if gajim.capscache.is_supported(self.contact, NS_JINGLE_RTP_AUDIO):
|
||||
if self.contact.supports(NS_JINGLE_RTP_AUDIO):
|
||||
if self.audio_state == self.JINGLE_STATE_NOT_AVAILABLE:
|
||||
self.set_audio_state('available')
|
||||
else:
|
||||
self.set_audio_state('not_available')
|
||||
|
||||
if gajim.capscache.is_supported(self.contact, NS_JINGLE_RTP_VIDEO):
|
||||
if self.contact.supports(NS_JINGLE_RTP_VIDEO):
|
||||
if self.video_state == self.JINGLE_STATE_NOT_AVAILABLE:
|
||||
self.set_video_state('available')
|
||||
else:
|
||||
|
@ -1403,12 +1401,11 @@ class ChatControl(ChatControlBase):
|
|||
self._video_button.set_sensitive(True)
|
||||
|
||||
# Send file
|
||||
if gajim.capscache.is_supported(self.contact, NS_FILE) and \
|
||||
self.contact.resource:
|
||||
if self.contact.supports(NS_FILE) and self.contact.resource:
|
||||
self._send_file_button.set_sensitive(True)
|
||||
else:
|
||||
self._send_file_button.set_sensitive(False)
|
||||
if not gajim.capscache.is_supported(self.contact, NS_FILE):
|
||||
if not self.contact.supports(NS_FILE):
|
||||
self._send_file_button.set_tooltip_text(_(
|
||||
"This contact does not support file transfer."))
|
||||
else:
|
||||
|
@ -1417,7 +1414,7 @@ class ChatControl(ChatControlBase):
|
|||
"her a file."))
|
||||
|
||||
# Convert to GC
|
||||
if gajim.capscache.is_supported(self.contact, NS_MUC):
|
||||
if self.contact.supports(NS_MUC):
|
||||
self._convert_to_gc_button.set_sensitive(True)
|
||||
else:
|
||||
self._convert_to_gc_button.set_sensitive(False)
|
||||
|
@ -1982,10 +1979,7 @@ class ChatControl(ChatControlBase):
|
|||
self._schedule_activity_timers()
|
||||
|
||||
def _on_sent(id_, contact, message, encrypted, xhtml):
|
||||
# XXX: Once we have fallback to disco, remove notexistant check
|
||||
if gajim.capscache.is_supported(contact, NS_RECEIPTS) \
|
||||
and not gajim.capscache.is_supported(contact,
|
||||
'notexistant') and gajim.config.get_per('accounts',
|
||||
if contact.supports(NS_RECEIPTS) and gajim.config.get_per('accounts',
|
||||
self.account, 'request_receipt'):
|
||||
xep0184_id = id_
|
||||
else:
|
||||
|
@ -2504,12 +2498,8 @@ class ChatControl(ChatControlBase):
|
|||
want_e2e = not e2e_is_active and not self.gpg_is_active \
|
||||
and e2e_pref
|
||||
|
||||
# XXX: Once we have fallback to disco, remove notexistant check
|
||||
if want_e2e and not self.no_autonegotiation \
|
||||
and gajim.HAVE_PYCRYPTO \
|
||||
and gajim.capscache.is_supported(self.contact,
|
||||
NS_ESESSION) and not gajim.capscache.is_supported(
|
||||
self.contact, 'notexistant'):
|
||||
and gajim.HAVE_PYCRYPTO and self.contact.supports(NS_ESESSION):
|
||||
self.begin_e2e_negotiation()
|
||||
else:
|
||||
self.send_chatstate('active', self.contact)
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
## Copyright (C) 2007-2008 Yann Leboulanger <asterix AT lagaule.org>
|
||||
## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
|
||||
## Jonathan Schleifer <js-gajim AT webkeks.org>
|
||||
## Stephan Erb <steve-e AT h3c.de>
|
||||
## Copyright (C) 2008-2009 Stephan Erb <steve-e AT h3c.de>
|
||||
##
|
||||
## This file is part of Gajim.
|
||||
##
|
||||
|
@ -23,55 +23,198 @@
|
|||
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
|
||||
##
|
||||
|
||||
from itertools import *
|
||||
'''
|
||||
Module containing all XEP-115 (Entity Capabilities) related classes
|
||||
|
||||
Basic Idea:
|
||||
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
|
||||
|
||||
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]
|
||||
|
||||
|
||||
capscache = None
|
||||
def initialize(logger):
|
||||
''' Initializes the capscache global '''
|
||||
global capscache
|
||||
capscache = CapsCache(logger)
|
||||
|
||||
|
||||
def compute_caps_hash(identities, features, dataforms=[], hash_method='sha-1'):
|
||||
'''Compute caps hash according to XEP-0115, V1.5
|
||||
|
||||
dataforms are xmpp.DataForms objects as common.dataforms don't allow several
|
||||
values without a field type list-multi'''
|
||||
|
||||
def sort_identities_func(i1, i2):
|
||||
cat1 = i1['category']
|
||||
cat2 = i2['category']
|
||||
if cat1 < cat2:
|
||||
return -1
|
||||
if cat1 > cat2:
|
||||
return 1
|
||||
type1 = i1.get('type', '')
|
||||
type2 = i2.get('type', '')
|
||||
if type1 < type2:
|
||||
return -1
|
||||
if type1 > type2:
|
||||
return 1
|
||||
lang1 = i1.get('xml:lang', '')
|
||||
lang2 = i2.get('xml:lang', '')
|
||||
if lang1 < lang2:
|
||||
return -1
|
||||
if lang1 > lang2:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def sort_dataforms_func(d1, d2):
|
||||
f1 = d1.getField('FORM_TYPE')
|
||||
f2 = d2.getField('FORM_TYPE')
|
||||
if f1 and f2 and (f1.getValue() < f2.getValue()):
|
||||
return -1
|
||||
return 1
|
||||
|
||||
S = ''
|
||||
identities.sort(cmp=sort_identities_func)
|
||||
for i in identities:
|
||||
c = i['category']
|
||||
type_ = i.get('type', '')
|
||||
lang = i.get('xml:lang', '')
|
||||
name = i.get('name', '')
|
||||
S += '%s/%s/%s/%s<' % (c, type_, lang, name)
|
||||
features.sort()
|
||||
for f in features:
|
||||
S += '%s<' % f
|
||||
dataforms.sort(cmp=sort_dataforms_func)
|
||||
for dataform in dataforms:
|
||||
# fields indexed by var
|
||||
fields = {}
|
||||
for f in dataform.getChildren():
|
||||
fields[f.getVar()] = f
|
||||
form_type = fields.get('FORM_TYPE')
|
||||
if form_type:
|
||||
S += form_type.getValue() + '<'
|
||||
del fields['FORM_TYPE']
|
||||
for var in sorted(fields.keys()):
|
||||
S += '%s<' % var
|
||||
values = sorted(fields[var].getValues())
|
||||
for value in values:
|
||||
S += '%s<' % value
|
||||
|
||||
if hash_method == 'sha-1':
|
||||
hash_ = hashlib.sha1(S)
|
||||
elif hash_method == 'md5':
|
||||
hash_ = hashlib.md5(S)
|
||||
else:
|
||||
return ''
|
||||
return base64.b64encode(hash_.digest())
|
||||
|
||||
|
||||
class AbstractClientCaps(object):
|
||||
'''
|
||||
Base class representing a client and its capabilities as advertised by
|
||||
a caps tag in a presence.
|
||||
'''
|
||||
def __init__(self, caps_hash, node):
|
||||
self._hash = caps_hash
|
||||
self._node = node
|
||||
|
||||
def get_discover_strategy(self):
|
||||
return self._discover
|
||||
|
||||
def _discover(self, connection, jid):
|
||||
''' To be implemented by subclassess '''
|
||||
raise NotImplementedError()
|
||||
|
||||
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()
|
||||
|
||||
def get_hash_validation_strategy(self):
|
||||
return self._is_hash_valid
|
||||
|
||||
def _is_hash_valid(self, identities, features, dataforms):
|
||||
''' To be implemented by subclassess '''
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
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'
|
||||
self._hash_method = hash_method
|
||||
|
||||
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))
|
||||
|
||||
def _is_hash_valid(self, identities, features, dataforms):
|
||||
computed_hash = compute_caps_hash(identities, features,
|
||||
dataforms=dataforms, hash_method=self._hash_method)
|
||||
return computed_hash == self._hash
|
||||
|
||||
|
||||
class OldClientCaps(AbstractClientCaps):
|
||||
''' Old XEP-115 implemtation. Kept around for background competability. '''
|
||||
|
||||
def __init__(self, caps_hash, node):
|
||||
AbstractClientCaps.__init__(self, caps_hash, node)
|
||||
|
||||
def _lookup_in_cache(self, caps_cache):
|
||||
return caps_cache[('old', self._node + '#' + self._hash)]
|
||||
|
||||
def _discover(self, connection, jid):
|
||||
connection.discoverInfo(jid)
|
||||
|
||||
def _is_hash_valid(self, identities, features, dataforms):
|
||||
return True
|
||||
|
||||
|
||||
class NullClientCaps(AbstractClientCaps):
|
||||
'''
|
||||
This is a NULL-Object to streamline caps handling if a client has not
|
||||
advertised any caps or has advertised them in an improper way.
|
||||
|
||||
Assumes (almost) everything is supported.
|
||||
'''
|
||||
|
||||
def __init__(self):
|
||||
AbstractClientCaps.__init__(self, None, None)
|
||||
|
||||
def _lookup_in_cache(self, caps_cache):
|
||||
# lookup something which does not exist to get a new CacheItem created
|
||||
cache_item = caps_cache[('old', '')]
|
||||
assert cache_item.queried == 0
|
||||
return cache_item
|
||||
|
||||
def _discover(self, connection, jid):
|
||||
pass
|
||||
|
||||
def _is_hash_valid(self, identities, features, dataforms):
|
||||
return False
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Connections with other objects: (TODO)
|
||||
|
||||
Interface:
|
||||
|
||||
# object creation
|
||||
>>> cc=CapsCache(logger_object)
|
||||
|
||||
>>> caps = ('sha-1', '66/0NaeaBKkwk85efJTGmU47vXI=')
|
||||
>>> muc = 'http://jabber.org/protocol/muc'
|
||||
>>> chatstates = 'http://jabber.org/protocol/chatstates'
|
||||
|
||||
# setting data
|
||||
>>> cc[caps].identities = [{'category':'client', 'type':'pc'}]
|
||||
>>> cc[caps].features = [muc]
|
||||
|
||||
# retrieving data
|
||||
>>> muc in cc[caps].features
|
||||
True
|
||||
>>> chatstates in cc[caps].features
|
||||
False
|
||||
>>> cc[caps].identities
|
||||
[{'category': 'client', 'type': 'pc'}]
|
||||
>>> x = cc[caps] # more efficient if making several queries for one set of caps
|
||||
ATypicalBlackBoxObject
|
||||
>>> muc in x.features
|
||||
True
|
||||
|
||||
'''
|
||||
def __init__(self, logger=None):
|
||||
''' Create a cache for entity capabilities. '''
|
||||
# our containers:
|
||||
# __cache is a dictionary mapping: pair of hash method and hash maps
|
||||
# to CapsCacheItem object
|
||||
|
@ -80,40 +223,39 @@ class CapsCache(object):
|
|||
self.__cache = {}
|
||||
|
||||
class CacheItem(object):
|
||||
''' TODO: logging data into db '''
|
||||
# __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)?
|
||||
__names = {}
|
||||
def __init__(ciself, hash_method, hash_):
|
||||
|
||||
def __init__(self, hash_method, hash_, logger):
|
||||
# cached into db
|
||||
ciself.hash_method = hash_method
|
||||
ciself.hash = hash_
|
||||
ciself._features = []
|
||||
ciself._identities = []
|
||||
self.hash_method = hash_method
|
||||
self.hash = hash_
|
||||
self._features = []
|
||||
self._identities = []
|
||||
self._logger = logger
|
||||
|
||||
# not cached into db:
|
||||
# have we sent the query?
|
||||
# 0 == not queried
|
||||
# 1 == queried
|
||||
# 2 == got the answer
|
||||
ciself.queried = 0
|
||||
self.queried = 0
|
||||
|
||||
def _get_features(ciself):
|
||||
return ciself._features
|
||||
def _get_features(self):
|
||||
return self._features
|
||||
|
||||
def _set_features(ciself, value):
|
||||
ciself._features = []
|
||||
def _set_features(self, value):
|
||||
self._features = []
|
||||
for feature in value:
|
||||
ciself._features.append(ciself.__names.setdefault(feature,
|
||||
feature))
|
||||
self._features.append(self.__names.setdefault(feature, feature))
|
||||
|
||||
features = property(_get_features, _set_features)
|
||||
|
||||
def _get_identities(ciself):
|
||||
def _get_identities(self):
|
||||
list_ = []
|
||||
for i in ciself._identities:
|
||||
for i in self._identities:
|
||||
# transforms it back in a dict
|
||||
d = dict()
|
||||
d['category'] = i[0]
|
||||
|
@ -125,36 +267,27 @@ class CapsCache(object):
|
|||
d['name'] = i[3]
|
||||
list_.append(d)
|
||||
return list_
|
||||
def _set_identities(ciself, value):
|
||||
ciself._identities = []
|
||||
|
||||
def _set_identities(self, value):
|
||||
self._identities = []
|
||||
for identity in value:
|
||||
# dict are not hashable, so transform it into a tuple
|
||||
t = (identity['category'], identity.get('type'),
|
||||
identity.get('xml:lang'), identity.get('name'))
|
||||
ciself._identities.append(ciself.__names.setdefault(t, t))
|
||||
self._identities.append(self.__names.setdefault(t, t))
|
||||
|
||||
identities = property(_get_identities, _set_identities)
|
||||
|
||||
def update(ciself, identities, features):
|
||||
# NOTE: self refers to CapsCache object, not to CacheItem
|
||||
ciself.identities=identities
|
||||
ciself.features=features
|
||||
self.logger.add_caps_entry(ciself.hash_method, ciself.hash,
|
||||
def set_and_store(self, identities, 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]
|
||||
gajimcaps.features = gajim.gajim_common_features + \
|
||||
gajim.gajim_optional_features[account]
|
||||
|
||||
# start logging data from the net
|
||||
self.logger = logger
|
||||
|
||||
def load_from_db(self):
|
||||
def initialize_from_db(self):
|
||||
# get data from logger...
|
||||
if self.logger is not None:
|
||||
for hash_method, hash_, identities, features in \
|
||||
|
@ -170,61 +303,35 @@ class CapsCache(object):
|
|||
|
||||
hash_method, hash_ = caps
|
||||
|
||||
x = self.__CacheItem(hash_method, hash_)
|
||||
x = self.__CacheItem(hash_method, hash_, self.logger)
|
||||
self.__cache[(hash_method, hash_)] = x
|
||||
return x
|
||||
|
||||
def preload(self, con, jid, node, hash_method, hash_):
|
||||
''' 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:
|
||||
def query_client_of_jid_if_unknown(self, connection, jid, client_caps):
|
||||
'''
|
||||
Start a disco query to determine caps (node, ver, exts).
|
||||
Won't query if the data is already in cache.
|
||||
'''
|
||||
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_))
|
||||
q.queried = 1
|
||||
discover = client_caps.get_discover_strategy()
|
||||
discover(connection, jid)
|
||||
|
||||
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
|
||||
|
||||
gajim.capscache = CapsCache(gajim.logger)
|
||||
|
||||
class ConnectionCaps(object):
|
||||
''' This class highly depends on that it is a part of Connection class. '''
|
||||
'''
|
||||
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'''
|
||||
|
||||
'''
|
||||
Handle incoming presence stanzas... This is a callback for xmpp
|
||||
registered in connection_handlers.py
|
||||
'''
|
||||
# we will put these into proper Contact object and ask
|
||||
# for disco... so that disco will learn how to interpret
|
||||
# these caps
|
||||
|
@ -245,64 +352,47 @@ class ConnectionCaps(object):
|
|||
# into Contacts
|
||||
return
|
||||
|
||||
# get the caps element
|
||||
caps = presence.getTag('c')
|
||||
if not caps:
|
||||
contact.caps_node = None
|
||||
contact.caps_hash = None
|
||||
contact.caps_hash_method = None
|
||||
return
|
||||
caps_tag = presence.getTag('c')
|
||||
if not caps_tag:
|
||||
# presence did not contain caps_tag
|
||||
client_caps = NullClientCaps()
|
||||
else:
|
||||
hash_method, node, caps_hash = caps_tag['hash'], caps_tag['node'], caps_tag['ver']
|
||||
|
||||
hash_method, node, hash_ = caps['hash'], caps['node'], caps['ver']
|
||||
if node is None or caps_hash is None:
|
||||
# improper caps in stanza, ignore client capabilities.
|
||||
client_caps = NullClientCaps()
|
||||
elif hash_method is None:
|
||||
client_caps = OldClientCaps(caps_hash, node)
|
||||
else:
|
||||
client_caps = ClientCaps(caps_hash, node, hash_method)
|
||||
|
||||
capscache.query_client_of_jid_if_unknown(self, jid, client_caps)
|
||||
contact.client_caps = client_caps
|
||||
|
||||
if hash_method is None and node and hash_:
|
||||
# Old XEP-115 implentation
|
||||
hash_method = 'old'
|
||||
|
||||
if hash_method is None or node is None or hash_ is None:
|
||||
# improper caps in stanza, ignoring
|
||||
contact.caps_node = None
|
||||
contact.caps_hash = None
|
||||
contact.hash_method = None
|
||||
return
|
||||
|
||||
# start disco query...
|
||||
gajim.capscache.preload(self, jid, node, hash_method, hash_)
|
||||
|
||||
# overwriting old data
|
||||
contact.caps_node = node
|
||||
contact.caps_hash_method = hash_method
|
||||
contact.caps_hash = hash_
|
||||
if pm_ctrl:
|
||||
pm_ctrl.update_contact()
|
||||
|
||||
def _capsDiscoCB(self, jid, node, identities, features, dataforms):
|
||||
def _capsDiscoCB(self, jid, node, identities, features, dataforms):
|
||||
contact = gajim.contacts.get_contact_from_full_jid(self.name, jid)
|
||||
if not contact:
|
||||
room_jid, nick = gajim.get_room_and_nick_from_fjid(jid)
|
||||
contact = gajim.contacts.get_gc_contact(self.name, room_jid, nick)
|
||||
if contact is None:
|
||||
return
|
||||
if not contact.caps_node:
|
||||
return # we didn't asked for that?
|
||||
if contact.caps_hash_method != 'old':
|
||||
computed_hash = helpers.compute_caps_hash(identities, features,
|
||||
dataforms=dataforms, hash_method=contact.caps_hash_method)
|
||||
if computed_hash != contact.caps_hash:
|
||||
# wrong hash, forget it
|
||||
contact.caps_node = ''
|
||||
contact.caps_hash_method = ''
|
||||
contact.caps_hash = ''
|
||||
return
|
||||
# if we don't have this info already...
|
||||
caps = gajim.capscache[(contact.caps_hash_method, contact.caps_hash)]
|
||||
else:
|
||||
# if we don't have this info already...
|
||||
caps = gajim.capscache[(contact.caps_hash_method, contact.caps_node + \
|
||||
'#' + contact.caps_hash)]
|
||||
if caps.queried == 2:
|
||||
return
|
||||
|
||||
caps.update(identities, features)
|
||||
lookup = contact.client_caps.get_cache_lookup_strategy()
|
||||
cache_item = lookup(capscache)
|
||||
|
||||
if cache_item.queried == 2:
|
||||
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()
|
||||
|
||||
# vim: se ts=3:
|
||||
|
|
|
@ -1299,14 +1299,9 @@ class Connection(ConnectionHandlers):
|
|||
# please note that the only valid tag inside a message containing a <body>
|
||||
# tag is the active event
|
||||
if chatstate is not None:
|
||||
# XXX: Once we have fallback to disco,
|
||||
# remove notexistant check
|
||||
if ((composing_xep == 'XEP-0085' or not composing_xep) \
|
||||
and composing_xep != 'asked_once') or \
|
||||
(gajim.capscache.is_supported(contact,
|
||||
common.xmpp.NS_CHATSTATES) and \
|
||||
not gajim.capscache.is_supported(contact,
|
||||
'notexistant')):
|
||||
contact.supports(common.xmpp.NS_CHATSTATES):
|
||||
# XEP-0085
|
||||
msg_iq.setTag(chatstate, namespace=common.xmpp.NS_CHATSTATES)
|
||||
if composing_xep in ('XEP-0022', 'asked_once') or \
|
||||
|
@ -1332,8 +1327,7 @@ class Connection(ConnectionHandlers):
|
|||
|
||||
# XEP-0184
|
||||
if msgtxt and gajim.config.get_per('accounts', self.name,
|
||||
'request_receipt') and gajim.capscache.is_supported(contact,
|
||||
common.xmpp.NS_RECEIPTS):
|
||||
'request_receipt') and contact.supports(common.xmpp.NS_RECEIPTS):
|
||||
msg_iq.setTag('request', namespace=common.xmpp.NS_RECEIPTS)
|
||||
|
||||
if session:
|
||||
|
|
|
@ -2469,9 +2469,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
|
|||
if not sess.received_thread_id:
|
||||
contact = gajim.contacts.get_contact(self.name, jid)
|
||||
|
||||
session_supported = gajim.capscache.is_supported(contact,
|
||||
common.xmpp.NS_SSN) or gajim.capscache.is_supported(
|
||||
contact, common.xmpp.NS_ESESSION)
|
||||
session_supported = contact.supports(common.xmpp.NS_SSN) or \
|
||||
contact.supports(common.xmpp.NS_ESESSION)
|
||||
if session_supported:
|
||||
sess.terminate()
|
||||
del self.sessions[jid][sess.thread_id]
|
||||
|
|
|
@ -30,31 +30,23 @@
|
|||
|
||||
import common.gajim
|
||||
|
||||
class Contact:
|
||||
'''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={},
|
||||
activity={}):
|
||||
|
||||
from common import caps
|
||||
|
||||
|
||||
class CommonContact(object):
|
||||
|
||||
def __init__(self, jid, resource, show, status, name, our_chatstate,
|
||||
composing_xep, chatstate, client_caps=None):
|
||||
|
||||
self.jid = jid
|
||||
self.name = name
|
||||
self.contact_name = '' # nick choosen by contact
|
||||
self.groups = groups
|
||||
self.resource = resource
|
||||
self.show = show
|
||||
self.status = status
|
||||
self.sub = sub
|
||||
self.ask = ask
|
||||
self.resource = resource
|
||||
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
|
||||
|
||||
self.name = name
|
||||
|
||||
self.client_caps = client_caps or caps.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:
|
||||
# None, False and 'ask'
|
||||
|
@ -63,14 +55,69 @@ class Contact:
|
|||
# 'ask' if we sent the first 'active' chatstate and are waiting for reply
|
||||
# this holds what WE SEND to contact (our current chatstate)
|
||||
self.our_chatstate = our_chatstate
|
||||
self.msg_id = msg_id
|
||||
# tell which XEP we're using for composing state
|
||||
# None = have to ask, XEP-0022 = use this xep,
|
||||
# XEP-0085 = use this xep, False = no composing support
|
||||
self.composing_xep = composing_xep
|
||||
# this is contact's chatstate
|
||||
self.chatstate = chatstate
|
||||
|
||||
def get_full_jid(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_shown_name(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def supports(self, requested_feature):
|
||||
'''
|
||||
Returns True if the contact has advertised to support the feature
|
||||
identified by the given namespace. False otherwise.
|
||||
'''
|
||||
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(caps.capscache)
|
||||
|
||||
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 caps.FEATURE_BLACKLIST
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class Contact(CommonContact):
|
||||
'''Information concerning each contact'''
|
||||
def __init__(self, jid='', name='', groups=[], show='', status='', sub='',
|
||||
ask='', resource='', priority=0, keyID='', client_caps=None,
|
||||
our_chatstate=None, chatstate=None, last_status_time=None, msg_id = None,
|
||||
composing_xep=None, mood={}, tune={}, activity={}):
|
||||
|
||||
CommonContact.__init__(self, jid, resource, show, status, name,
|
||||
our_chatstate, composing_xep, chatstate, client_caps=client_caps)
|
||||
|
||||
self.contact_name = '' # nick choosen by contact
|
||||
self.groups = groups
|
||||
|
||||
self.sub = sub
|
||||
self.ask = ask
|
||||
|
||||
self.priority = priority
|
||||
self.keyID = keyID
|
||||
self.msg_id = msg_id
|
||||
self.last_status_time = last_status_time
|
||||
|
||||
self.mood = mood.copy()
|
||||
self.tune = tune.copy()
|
||||
self.activity = activity.copy()
|
||||
|
@ -135,31 +182,25 @@ class Contact:
|
|||
return False
|
||||
|
||||
|
||||
class GC_Contact:
|
||||
class GC_Contact(CommonContact):
|
||||
'''Information concerning each groupchat contact'''
|
||||
def __init__(self, room_jid='', name='', show='', status='', role='',
|
||||
affiliation='', jid = '', resource = '', our_chatstate = None,
|
||||
composing_xep = None, chatstate = None):
|
||||
affiliation='', jid='', resource='', our_chatstate=None,
|
||||
composing_xep=None, chatstate=None):
|
||||
|
||||
CommonContact.__init__(self, jid, resource, show, status, name,
|
||||
our_chatstate, composing_xep, chatstate)
|
||||
|
||||
self.room_jid = room_jid
|
||||
self.name = name
|
||||
self.show = show
|
||||
self.status = status
|
||||
self.role = role
|
||||
self.affiliation = affiliation
|
||||
self.jid = jid
|
||||
self.resource = resource
|
||||
self.caps_node = None
|
||||
self.caps_hash_method = None
|
||||
self.caps_hash = None
|
||||
self.our_chatstate = our_chatstate
|
||||
self.composing_xep = composing_xep
|
||||
self.chatstate = chatstate
|
||||
|
||||
|
||||
def get_full_jid(self):
|
||||
return self.room_jid + '/' + self.name
|
||||
|
||||
def get_shown_name(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Contacts:
|
||||
'''Information concerning all contacts and groupchat contacts'''
|
||||
|
@ -193,10 +234,9 @@ 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,
|
||||
chatstate=None, last_status_time=None, composing_xep=None,
|
||||
mood={}, tune={}, activity={}):
|
||||
sub='', ask='', resource='', priority=0, keyID='', client_caps=None,
|
||||
our_chatstate=None, chatstate=None, last_status_time=None,
|
||||
composing_xep=None, mood={}, tune={}, activity={}):
|
||||
|
||||
# We don't want duplicated group values
|
||||
groups_unique = []
|
||||
|
@ -206,18 +246,16 @@ 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,
|
||||
last_status_time=last_status_time, composing_xep=composing_xep,
|
||||
mood=mood, tune=tune, activity=activity)
|
||||
keyID=keyID, client_caps=client_caps, our_chatstate=our_chatstate,
|
||||
chatstate=chatstate, last_status_time=last_status_time,
|
||||
composing_xep=composing_xep, mood=mood, tune=tune, activity=activity)
|
||||
|
||||
def copy_contact(self, contact):
|
||||
return self.create_contact(jid=contact.jid, name=contact.name,
|
||||
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, our_chatstate=contact.our_chatstate,
|
||||
chatstate=contact.chatstate, last_status_time=contact.last_status_time)
|
||||
|
||||
def add_contact(self, account, contact):
|
||||
|
@ -587,9 +625,7 @@ 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)
|
||||
|
||||
def create_gc_contact(self, room_jid='', name='', show='', status='',
|
||||
role='', affiliation='', jid='', resource=''):
|
||||
|
|
|
@ -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
|
||||
|
@ -209,6 +210,9 @@ gajim_optional_features = {}
|
|||
# Capabilities hash per account
|
||||
caps_hash = {}
|
||||
|
||||
import caps
|
||||
caps.initialize(logger)
|
||||
|
||||
def get_nick_from_jid(jid):
|
||||
pos = jid.find('@')
|
||||
return jid[:pos]
|
||||
|
|
|
@ -37,8 +37,8 @@ import urllib
|
|||
import errno
|
||||
import select
|
||||
import base64
|
||||
import sys
|
||||
import hashlib
|
||||
import caps
|
||||
|
||||
from encodings.punycode import punycode_encode
|
||||
|
||||
|
@ -566,74 +566,6 @@ def datetime_tuple(timestamp):
|
|||
from time import strptime
|
||||
return strptime(timestamp, '%Y%m%dT%H:%M:%S')
|
||||
|
||||
def sort_identities_func(i1, i2):
|
||||
cat1 = i1['category']
|
||||
cat2 = i2['category']
|
||||
if cat1 < cat2:
|
||||
return -1
|
||||
if cat1 > cat2:
|
||||
return 1
|
||||
type1 = i1.get('type', '')
|
||||
type2 = i2.get('type', '')
|
||||
if type1 < type2:
|
||||
return -1
|
||||
if type1 > type2:
|
||||
return 1
|
||||
lang1 = i1.get('xml:lang', '')
|
||||
lang2 = i2.get('xml:lang', '')
|
||||
if lang1 < lang2:
|
||||
return -1
|
||||
if lang1 > lang2:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def sort_dataforms_func(d1, d2):
|
||||
f1 = d1.getField('FORM_TYPE')
|
||||
f2 = d2.getField('FORM_TYPE')
|
||||
if f1 and f2 and (f1.getValue() < f2.getValue()):
|
||||
return -1
|
||||
return 1
|
||||
|
||||
def compute_caps_hash(identities, features, dataforms=[], hash_method='sha-1'):
|
||||
'''Compute caps hash according to XEP-0115, V1.5
|
||||
|
||||
dataforms are xmpp.DataForms objects as common.dataforms don't allow several
|
||||
values without a field type list-multi'''
|
||||
S = ''
|
||||
identities.sort(cmp=sort_identities_func)
|
||||
for i in identities:
|
||||
c = i['category']
|
||||
type_ = i.get('type', '')
|
||||
lang = i.get('xml:lang', '')
|
||||
name = i.get('name', '')
|
||||
S += '%s/%s/%s/%s<' % (c, type_, lang, name)
|
||||
features.sort()
|
||||
for f in features:
|
||||
S += '%s<' % f
|
||||
dataforms.sort(cmp=sort_dataforms_func)
|
||||
for dataform in dataforms:
|
||||
# fields indexed by var
|
||||
fields = {}
|
||||
for f in dataform.getChildren():
|
||||
fields[f.getVar()] = f
|
||||
form_type = fields.get('FORM_TYPE')
|
||||
if form_type:
|
||||
S += form_type.getValue() + '<'
|
||||
del fields['FORM_TYPE']
|
||||
for var in sorted(fields.keys()):
|
||||
S += '%s<' % var
|
||||
values = sorted(fields[var].getValues())
|
||||
for value in values:
|
||||
S += '%s<' % value
|
||||
|
||||
if hash_method == 'sha-1':
|
||||
hash_ = hashlib.sha1(S)
|
||||
elif hash_method == 'md5':
|
||||
hash_ = hashlib.md5(S)
|
||||
else:
|
||||
return ''
|
||||
return base64.b64encode(hash_.digest())
|
||||
|
||||
# import gajim only when needed (after decode_string is defined) see #4764
|
||||
|
||||
import gajim
|
||||
|
@ -1363,7 +1295,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] = compute_caps_hash([gajim.gajim_identity],
|
||||
gajim.caps_hash[a] = caps.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
|
||||
|
|
|
@ -31,6 +31,7 @@ import locale
|
|||
import re
|
||||
from common import gajim
|
||||
from common import helpers
|
||||
from common import caps
|
||||
|
||||
import exceptions
|
||||
try:
|
||||
|
@ -219,7 +220,7 @@ class OptionsParser:
|
|||
gajim.logger.init_vars()
|
||||
gajim.config.set('version', new_version)
|
||||
|
||||
gajim.capscache.load_from_db()
|
||||
caps.capscache.initialize_from_db()
|
||||
|
||||
def update_config_x_to_09(self):
|
||||
# Var name that changed:
|
||||
|
|
|
@ -223,7 +223,7 @@ class EncryptedStanzaSession(StanzaSession):
|
|||
|
||||
def _is_buggy_gajim(self):
|
||||
c = self._get_contact()
|
||||
if gajim.capscache.is_supported(c, xmpp.NS_ROSTERX):
|
||||
if c and c.supports(xmpp.NS_ROSTERX):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
|
|
@ -255,6 +255,7 @@ from common import optparser
|
|||
from common import dataforms
|
||||
from common import passwords
|
||||
from common import pep
|
||||
from common import caps
|
||||
|
||||
gajimpaths = common.configpaths.gajimpaths
|
||||
|
||||
|
@ -3641,6 +3642,12 @@ class Interface:
|
|||
gajim.caps_hash[a] = ''
|
||||
|
||||
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.identities = [gajim.gajim_identity]
|
||||
gajimcaps.features = gajim.gajim_common_features + \
|
||||
gajim.gajim_optional_features[account]
|
||||
|
||||
self.remote_ctrl = None
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ room_account=None, cap=None):
|
|||
else: # start_chat, execute_command, send_file
|
||||
item.connect('activate', action, c, account, c.resource)
|
||||
|
||||
if cap and not gajim.capscache.is_supported(c, cap):
|
||||
if cap and not c.supports(cap):
|
||||
item.set_sensitive(False)
|
||||
|
||||
return sub_menu
|
||||
|
@ -92,7 +92,7 @@ def build_invite_submenu(invite_menuitem, list_):
|
|||
if len(contact_list) > 1: # several resources
|
||||
invite_to_new_room_menuitem.set_submenu(build_resources_submenu(
|
||||
contact_list, account, roster.on_invite_to_new_room, cap=NS_MUC))
|
||||
elif len(list_) == 1 and gajim.capscache.is_supported(contact, NS_MUC):
|
||||
elif len(list_) == 1 and contact.supports(NS_MUC):
|
||||
invite_menuitem.set_sensitive(True)
|
||||
# use resource if it's self contact
|
||||
if contact.jid == gajim.get_jid_from_account(account):
|
||||
|
@ -222,14 +222,14 @@ control=None):
|
|||
else:
|
||||
start_chat_menuitem.connect('activate',
|
||||
gajim.interface.on_open_chat_window, contact, account)
|
||||
if gajim.capscache.is_supported(contact, NS_FILE):
|
||||
if contact.supports(NS_FILE):
|
||||
send_file_menuitem.set_sensitive(True)
|
||||
send_file_menuitem.connect('activate',
|
||||
roster.on_send_file_menuitem_activate, contact, account)
|
||||
else:
|
||||
send_file_menuitem.set_sensitive(False)
|
||||
|
||||
if gajim.capscache.is_supported(contact, NS_COMMANDS):
|
||||
if contact.supports(NS_COMMANDS):
|
||||
execute_command_menuitem.set_sensitive(True)
|
||||
execute_command_menuitem.connect('activate', roster.on_execute_command,
|
||||
contact, account, contact.resource)
|
||||
|
@ -294,10 +294,7 @@ control=None):
|
|||
control._on_toggle_gpg_menuitem_activate)
|
||||
|
||||
# disable esessions if we or the other client don't support them
|
||||
# XXX: Once we have fallback to disco, remove notexistant check
|
||||
if not gajim.HAVE_PYCRYPTO or \
|
||||
not gajim.capscache.is_supported(contact, NS_ESESSION) or \
|
||||
gajim.capscache.is_supported(contact, 'notexistant') or \
|
||||
if not gajim.HAVE_PYCRYPTO or not contact.supports(NS_ESESSION) or \
|
||||
not gajim.config.get_per('accounts', account, 'enable_esessions'):
|
||||
toggle_e2e_menuitem.set_sensitive(False)
|
||||
else:
|
||||
|
|
|
@ -4061,7 +4061,7 @@ class RosterWindow:
|
|||
return
|
||||
c_dest = gajim.contacts.get_contact_with_highest_priority(account_dest,
|
||||
jid_dest)
|
||||
if not gajim.capscache.is_supported(c_dest, NS_FILE):
|
||||
if not c_dest.supports(NS_FILE):
|
||||
return
|
||||
uri = data.strip()
|
||||
uri_splitted = uri.split() # we may have more than one file dropped
|
||||
|
@ -5157,17 +5157,6 @@ class RosterWindow:
|
|||
|
||||
zeroconf_properties_menuitem.connect('activate',
|
||||
self.on_zeroconf_properties, account)
|
||||
#gc_sub_menu = gtk.Menu() # gc is always a submenu
|
||||
#join_group_chat_menuitem.set_submenu(gc_sub_menu)
|
||||
#self.add_bookmarks_list(gc_sub_menu, account)
|
||||
#new_message_menuitem.connect('activate',
|
||||
# self.on_new_message_menuitem_activate, account)
|
||||
|
||||
# make some items insensitive if account is offline
|
||||
#if gajim.connections[account].connected < 2:
|
||||
# for widget in [join_group_chat_menuitem, new_message_menuitem]:
|
||||
# widget.set_sensitive(False)
|
||||
# new_message_menuitem.set_sensitive(False)
|
||||
|
||||
return account_context_menu
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ import threading, time
|
|||
from mock import Mock
|
||||
|
||||
from common.xmpp import idlequeue
|
||||
from common.xmpp.plugin import PlugIn
|
||||
|
||||
IDLEQUEUE_INTERVAL = 0.2 # polling interval. 200ms is used in Gajim as default
|
||||
IDLEMOCK_TIMEOUT = 30 # how long we wait for an event
|
||||
|
|
|
@ -40,6 +40,7 @@ modules = ( 'test_xmpp_dispatcher_nb',
|
|||
'test_xmpp_transports_nb',
|
||||
'test_resolver',
|
||||
'test_caps',
|
||||
'test_contacts',
|
||||
)
|
||||
#modules = ()
|
||||
|
||||
|
|
|
@ -6,57 +6,148 @@ import unittest
|
|||
import lib
|
||||
lib.setup_env()
|
||||
|
||||
from common import gajim
|
||||
from common import xmpp
|
||||
from common import helpers
|
||||
|
||||
from common.caps import CapsCache
|
||||
from common.xmpp import NS_MUC, NS_PING, NS_XHTML_IM
|
||||
from common import caps
|
||||
from common.contacts import Contact
|
||||
|
||||
from mock import Mock
|
||||
|
||||
class MockLogger(Mock):
|
||||
def __init__(self, *args):
|
||||
Mock.__init__(self, *args)
|
||||
|
||||
class TestCapsCache(unittest.TestCase):
|
||||
|
||||
class CommonCapsTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.logger = MockLogger()
|
||||
self.cc = CapsCache(self.logger)
|
||||
|
||||
self.caps_method = 'sha-1'
|
||||
self.caps_hash = 'zaQfb22o0UCwYDIk8KZOnoZTnrs='
|
||||
self.caps = (self.caps_method, self.caps_hash)
|
||||
self.identity = {'category': 'client', 'type': 'pc'}
|
||||
|
||||
self.muc = 'http://jabber.org/protocol/muc'
|
||||
self.chatstates = 'http://jabber.org/protocol/chatstates'
|
||||
self.caps_hash = 'm3P2WeXPMGVH2tZPe7yITnfY0Dw='
|
||||
self.client_caps = (self.caps_method, self.caps_hash)
|
||||
|
||||
self.node = "http://gajim.org"
|
||||
self.identity = {'category': 'client', 'type': 'pc', 'name':'Gajim'}
|
||||
|
||||
self.identities = [self.identity]
|
||||
self.features = [self.muc]
|
||||
self.features = [NS_MUC, NS_XHTML_IM] # NS_MUC not supported!
|
||||
|
||||
# Simulate a filled db
|
||||
db_caps_cache = [
|
||||
(self.caps_method, self.caps_hash, self.identities, self.features),
|
||||
('old', self.node + '#' + self.caps_hash, self.identities, self.features)]
|
||||
self.logger = Mock(returnValues={"iter_caps_data":db_caps_cache})
|
||||
|
||||
self.cc = caps.CapsCache(self.logger)
|
||||
caps.capscache = self.cc
|
||||
|
||||
|
||||
class TestCapsCache(CommonCapsTest):
|
||||
|
||||
def test_set_retrieve(self):
|
||||
''' Test basic set / retrieve cycle '''
|
||||
|
||||
def test_examples(self):
|
||||
'''tests the examples given in common/caps.py'''
|
||||
self.cc[self.client_caps].identities = self.identities
|
||||
self.cc[self.client_caps].features = self.features
|
||||
|
||||
self.cc[self.caps].identities = self.identities
|
||||
self.cc[self.caps].features = self.features
|
||||
self.assert_(NS_MUC in self.cc[self.client_caps].features)
|
||||
self.assert_(NS_PING not in self.cc[self.client_caps].features)
|
||||
|
||||
self.assert_(self.muc in self.cc[self.caps].features)
|
||||
self.assert_(self.chatstates not in self.cc[self.caps].features)
|
||||
identities = self.cc[self.client_caps].identities
|
||||
|
||||
id = self.cc[self.caps].identities
|
||||
self.assertEqual(1, len(identities))
|
||||
|
||||
self.assertEqual(1, len(id))
|
||||
identity = identities[0]
|
||||
self.assertEqual('client', identity['category'])
|
||||
self.assertEqual('pc', identity['type'])
|
||||
|
||||
def test_set_and_store(self):
|
||||
''' Test client_caps update gets logged into db '''
|
||||
|
||||
item = self.cc[self.client_caps]
|
||||
item.set_and_store(self.identities, self.features)
|
||||
|
||||
self.logger.mockCheckCall(0, "add_caps_entry", self.caps_method,
|
||||
self.caps_hash, self.identities, self.features)
|
||||
|
||||
def test_initialize_from_db(self):
|
||||
''' Read cashed dummy data from db '''
|
||||
self.assertEqual(self.cc[self.client_caps].queried, 0)
|
||||
self.cc.initialize_from_db()
|
||||
self.assertEqual(self.cc[self.client_caps].queried, 2)
|
||||
|
||||
id = id[0]
|
||||
self.assertEqual('client', id['category'])
|
||||
self.assertEqual('pc', id['type'])
|
||||
def test_preload_triggering_query(self):
|
||||
''' Make sure that preload issues a disco '''
|
||||
connection = Mock()
|
||||
client_caps = 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.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 = caps.ClientCaps(self.caps_hash, self.node, self.caps_method)
|
||||
|
||||
self.cc.initialize_from_db()
|
||||
self.cc.query_client_of_jid_if_unknown(connection, "test@gajim.org",
|
||||
client_caps)
|
||||
|
||||
self.assertEqual(0, len(connection.mockGetAllCalls()))
|
||||
|
||||
def test_hash(self):
|
||||
'''tests the hash computation'''
|
||||
computed_hash = helpers.compute_caps_hash(self.identities, self.features)
|
||||
|
||||
computed_hash = caps.compute_caps_hash(self.identities, self.features)
|
||||
self.assertEqual(self.caps_hash, computed_hash)
|
||||
|
||||
|
||||
class TestClientCaps(CommonCapsTest):
|
||||
|
||||
def setUp(self):
|
||||
CommonCapsTest.setUp(self)
|
||||
self.client_caps = caps.ClientCaps(self.caps_hash, self.node, self.caps_method)
|
||||
|
||||
def test_query_by_get_discover_strategy(self):
|
||||
''' Client must be queried if the data is unkown '''
|
||||
connection = Mock()
|
||||
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_client_supports(self):
|
||||
contact = Contact(client_caps=self.client_caps)
|
||||
|
||||
self.assertTrue(contact.supports(NS_PING),
|
||||
msg="Assume supported, if we don't have caps")
|
||||
|
||||
self.assertFalse(contact.supports(NS_XHTML_IM),
|
||||
msg="Must not assume blacklisted feature is supported on default")
|
||||
|
||||
self.cc.initialize_from_db()
|
||||
|
||||
self.assertFalse(contact.supports(NS_PING),
|
||||
msg="Must return false on unsupported feature")
|
||||
|
||||
self.assertTrue(contact.supports(NS_XHTML_IM),
|
||||
msg="Must return True on supported feature")
|
||||
|
||||
self.assertTrue(contact.supports(NS_MUC),
|
||||
msg="Must return True on supported feature")
|
||||
|
||||
|
||||
class TestOldClientCaps(TestClientCaps):
|
||||
|
||||
def setUp(self):
|
||||
TestClientCaps.setUp(self)
|
||||
self.client_caps = caps.OldClientCaps(self.caps_hash, self.node)
|
||||
|
||||
def test_query_by_get_discover_strategy(self):
|
||||
''' Client must be queried if the data is unknown '''
|
||||
connection = Mock()
|
||||
discover = self.client_caps.get_discover_strategy()
|
||||
discover(connection, "test@gajim.org")
|
||||
|
||||
connection.mockCheckCall(0, "discoverInfo", "test@gajim.org")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
|
|
51
test/test_contacts.py
Normal file
51
test/test_contacts.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
'''
|
||||
Test for Contact, GC_Contact and Contacts
|
||||
'''
|
||||
import unittest
|
||||
|
||||
import lib
|
||||
lib.setup_env()
|
||||
|
||||
from common.contacts import CommonContact, Contact, GC_Contact
|
||||
from common.xmpp import NS_MUC
|
||||
|
||||
from common import caps
|
||||
|
||||
class TestCommonContact(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.contact = CommonContact(jid='', resource='', show='', status='',
|
||||
name='', our_chatstate=None, composing_xep=None, chatstate=None,
|
||||
client_caps=None)
|
||||
|
||||
def test_default_client_supports(self):
|
||||
'''
|
||||
Test the caps support method of contacts.
|
||||
See test_caps for more enhanced tests.
|
||||
'''
|
||||
caps.capscache = caps.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.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()
|
||||
|
||||
|
||||
class TestGC_Contact(TestCommonContact):
|
||||
|
||||
def setUp(self):
|
||||
TestCommonContact.setUp(self)
|
||||
self.contact = GC_Contact()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
|
@ -9,7 +9,6 @@ from mock import Mock, expectParams
|
|||
from gajim_mocks import *
|
||||
|
||||
from common import gajim
|
||||
from common import zeroconf
|
||||
import roster_window
|
||||
|
||||
gajim.get_jid_from_account = lambda acc: 'myjid@' + acc
|
||||
|
|
Loading…
Add table
Reference in a new issue