Merge refactoring branch back to default

This commit is contained in:
Stephan Erb 2009-10-31 18:08:06 +01:00
commit 07d0811439
18 changed files with 563 additions and 382 deletions

View file

@ -1,4 +1,5 @@
syntax: glob
*.orig
*.gmo
*.in
*.la

View file

@ -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)

View file

@ -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:

View file

@ -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:

View file

@ -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]

View file

@ -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=''):

View file

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

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -40,6 +40,7 @@ modules = ( 'test_xmpp_dispatcher_nb',
'test_xmpp_transports_nb',
'test_resolver',
'test_caps',
'test_contacts',
)
#modules = ()

View file

@ -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
View 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()

View file

@ -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