| 1 | # -*- coding:utf-8 -*- |
|---|
| 2 | ## src/common/caps.py |
|---|
| 3 | ## |
|---|
| 4 | ## Copyright (C) 2007 Tomasz Melcer <liori AT exroot.org> |
|---|
| 5 | ## Travis Shirk <travis AT pobox.com> |
|---|
| 6 | ## Copyright (C) 2007-2008 Yann Leboulanger <asterix AT lagaule.org> |
|---|
| 7 | ## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com> |
|---|
| 8 | ## Jonathan Schleifer <js-gajim AT webkeks.org> |
|---|
| 9 | ## Stephan Erb <steve-e AT h3c.de> |
|---|
| 10 | ## |
|---|
| 11 | ## This file is part of Gajim. |
|---|
| 12 | ## |
|---|
| 13 | ## Gajim is free software; you can redistribute it and/or modify |
|---|
| 14 | ## it under the terms of the GNU General Public License as published |
|---|
| 15 | ## by the Free Software Foundation; version 3 only. |
|---|
| 16 | ## |
|---|
| 17 | ## Gajim is distributed in the hope that it will be useful, |
|---|
| 18 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 19 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 20 | ## GNU General Public License for more details. |
|---|
| 21 | ## |
|---|
| 22 | ## You should have received a copy of the GNU General Public License |
|---|
| 23 | ## along with Gajim. If not, see <http://www.gnu.org/licenses/>. |
|---|
| 24 | ## |
|---|
| 25 | |
|---|
| 26 | from itertools import * |
|---|
| 27 | import gajim |
|---|
| 28 | import helpers |
|---|
| 29 | |
|---|
| 30 | class CapsCache(object): |
|---|
| 31 | ''' This object keeps the mapping between caps data and real disco |
|---|
| 32 | features they represent, and provides simple way to query that info. |
|---|
| 33 | It is application-wide, that is there's one object for all |
|---|
| 34 | connections. |
|---|
| 35 | Goals: |
|---|
| 36 | * handle storing/retrieving info from database |
|---|
| 37 | * cache info in memory |
|---|
| 38 | * expose simple interface |
|---|
| 39 | Properties: |
|---|
| 40 | * one object for all connections (move to logger.py?) |
|---|
| 41 | * store info efficiently (a set() of urls -- we can assume there won't be |
|---|
| 42 | too much of these, ensure that (X,Y,Z1) and (X,Y,Z2) has different |
|---|
| 43 | features. |
|---|
| 44 | |
|---|
| 45 | Connections with other objects: (TODO) |
|---|
| 46 | |
|---|
| 47 | Interface: |
|---|
| 48 | |
|---|
| 49 | # object creation |
|---|
| 50 | >>> cc=CapsCache(logger_object) |
|---|
| 51 | |
|---|
| 52 | >>> caps = ('sha-1', '66/0NaeaBKkwk85efJTGmU47vXI=') |
|---|
| 53 | >>> muc = 'http://jabber.org/protocol/muc' |
|---|
| 54 | >>> chatstates = 'http://jabber.org/protocol/chatstates' |
|---|
| 55 | |
|---|
| 56 | # setting data |
|---|
| 57 | >>> cc[caps].identities = [{'category':'client', 'type':'pc'}] |
|---|
| 58 | >>> cc[caps].features = [muc] |
|---|
| 59 | |
|---|
| 60 | # retrieving data |
|---|
| 61 | >>> muc in cc[caps].features |
|---|
| 62 | True |
|---|
| 63 | >>> chatstates in cc[caps].features |
|---|
| 64 | False |
|---|
| 65 | >>> cc[caps].identities |
|---|
| 66 | [{'category': 'client', 'type': 'pc'}] |
|---|
| 67 | >>> x = cc[caps] # more efficient if making several queries for one set of caps |
|---|
| 68 | ATypicalBlackBoxObject |
|---|
| 69 | >>> muc in x.features |
|---|
| 70 | True |
|---|
| 71 | |
|---|
| 72 | ''' |
|---|
| 73 | def __init__(self, logger=None): |
|---|
| 74 | ''' Create a cache for entity capabilities. ''' |
|---|
| 75 | # our containers: |
|---|
| 76 | # __cache is a dictionary mapping: pair of hash method and hash maps |
|---|
| 77 | # to CapsCacheItem object |
|---|
| 78 | # __CacheItem is a class that stores data about particular |
|---|
| 79 | # client (hash method/hash pair) |
|---|
| 80 | self.__cache = {} |
|---|
| 81 | |
|---|
| 82 | class CacheItem(object): |
|---|
| 83 | ''' TODO: logging data into db ''' |
|---|
| 84 | # __names is a string cache; every string long enough is given |
|---|
| 85 | # another object, and we will have plenty of identical long |
|---|
| 86 | # strings. therefore we can cache them |
|---|
| 87 | # TODO: maybe put all known xmpp namespace strings here |
|---|
| 88 | # (strings given in xmpppy)? |
|---|
| 89 | __names = {} |
|---|
| 90 | def __init__(ciself, hash_method, hash_): |
|---|
| 91 | # cached into db |
|---|
| 92 | ciself.hash_method = hash_method |
|---|
| 93 | ciself.hash = hash_ |
|---|
| 94 | ciself._features = [] |
|---|
| 95 | ciself._identities = [] |
|---|
| 96 | |
|---|
| 97 | # not cached into db: |
|---|
| 98 | # have we sent the query? |
|---|
| 99 | # 0 == not queried |
|---|
| 100 | # 1 == queried |
|---|
| 101 | # 2 == got the answer |
|---|
| 102 | ciself.queried = 0 |
|---|
| 103 | |
|---|
| 104 | def _get_features(ciself): |
|---|
| 105 | return ciself._features |
|---|
| 106 | |
|---|
| 107 | def _set_features(ciself, value): |
|---|
| 108 | ciself._features = [] |
|---|
| 109 | for feature in value: |
|---|
| 110 | ciself._features.append(ciself.__names.setdefault(feature, |
|---|
| 111 | feature)) |
|---|
| 112 | features = property(_get_features, _set_features) |
|---|
| 113 | |
|---|
| 114 | def _get_identities(ciself): |
|---|
| 115 | list_ = [] |
|---|
| 116 | for i in ciself._identities: |
|---|
| 117 | # transforms it back in a dict |
|---|
| 118 | d = dict() |
|---|
| 119 | d['category'] = i[0] |
|---|
| 120 | if i[1]: |
|---|
| 121 | d['type'] = i[1] |
|---|
| 122 | if i[2]: |
|---|
| 123 | d['xml:lang'] = i[2] |
|---|
| 124 | if i[3]: |
|---|
| 125 | d['name'] = i[3] |
|---|
| 126 | list_.append(d) |
|---|
| 127 | return list_ |
|---|
| 128 | def _set_identities(ciself, value): |
|---|
| 129 | ciself._identities = [] |
|---|
| 130 | for identity in value: |
|---|
| 131 | # dict are not hashable, so transform it into a tuple |
|---|
| 132 | t = (identity['category'], identity.get('type'), |
|---|
| 133 | identity.get('xml:lang'), identity.get('name')) |
|---|
| 134 | ciself._identities.append(ciself.__names.setdefault(t, t)) |
|---|
| 135 | identities = property(_get_identities, _set_identities) |
|---|
| 136 | |
|---|
| 137 | def update(ciself, identities, features): |
|---|
| 138 | # NOTE: self refers to CapsCache object, not to CacheItem |
|---|
| 139 | ciself.identities=identities |
|---|
| 140 | ciself.features=features |
|---|
| 141 | self.logger.add_caps_entry(ciself.hash_method, ciself.hash, |
|---|
| 142 | identities, features) |
|---|
| 143 | |
|---|
| 144 | self.__CacheItem = CacheItem |
|---|
| 145 | |
|---|
| 146 | # prepopulate data which we are sure of; note: we do not log these info |
|---|
| 147 | |
|---|
| 148 | for account in gajim.connections: |
|---|
| 149 | gajimcaps = self[('sha-1', gajim.caps_hash[account])] |
|---|
| 150 | gajimcaps.identities = [gajim.gajim_identity] |
|---|
| 151 | gajimcaps.features = gajim.gajim_common_features + \ |
|---|
| 152 | gajim.gajim_optional_features[account] |
|---|
| 153 | |
|---|
| 154 | # start logging data from the net |
|---|
| 155 | self.logger = logger |
|---|
| 156 | |
|---|
| 157 | def load_from_db(self): |
|---|
| 158 | # get data from logger... |
|---|
| 159 | if self.logger is not None: |
|---|
| 160 | for hash_method, hash_, identities, features in \ |
|---|
| 161 | self.logger.iter_caps_data(): |
|---|
| 162 | x = self[(hash_method, hash_)] |
|---|
| 163 | x.identities = identities |
|---|
| 164 | x.features = features |
|---|
| 165 | x.queried = 2 |
|---|
| 166 | |
|---|
| 167 | def __getitem__(self, caps): |
|---|
| 168 | if caps in self.__cache: |
|---|
| 169 | return self.__cache[caps] |
|---|
| 170 | |
|---|
| 171 | hash_method, hash_ = caps |
|---|
| 172 | |
|---|
| 173 | x = self.__CacheItem(hash_method, hash_) |
|---|
| 174 | self.__cache[(hash_method, hash_)] = x |
|---|
| 175 | return x |
|---|
| 176 | |
|---|
| 177 | def preload(self, con, jid, node, hash_method, hash_): |
|---|
| 178 | ''' Preload data about (node, ver, exts) caps using disco |
|---|
| 179 | query to jid using proper connection. Don't query if |
|---|
| 180 | the data is already in cache. ''' |
|---|
| 181 | if hash_method == 'old': |
|---|
| 182 | q = self[(hash_method, node + '#' + hash_)] |
|---|
| 183 | else: |
|---|
| 184 | q = self[(hash_method, hash_)] |
|---|
| 185 | |
|---|
| 186 | if q.queried==0: |
|---|
| 187 | # do query for bare node+hash pair |
|---|
| 188 | # this will create proper object |
|---|
| 189 | q.queried=1 |
|---|
| 190 | if hash_method == 'old': |
|---|
| 191 | con.discoverInfo(jid) |
|---|
| 192 | else: |
|---|
| 193 | con.discoverInfo(jid, '%s#%s' % (node, hash_)) |
|---|
| 194 | |
|---|
| 195 | def is_supported(self, contact, feature): |
|---|
| 196 | if not contact: |
|---|
| 197 | return False |
|---|
| 198 | |
|---|
| 199 | # Unfortunately, if all resources are offline, the contact |
|---|
| 200 | # includes the last resource that was online. Check for its |
|---|
| 201 | # show, so we can be sure it's existant. Otherwise, we still |
|---|
| 202 | # return caps for a contact that has no resources left. |
|---|
| 203 | if contact.show == 'offline': |
|---|
| 204 | return False |
|---|
| 205 | |
|---|
| 206 | # FIXME: We assume everything is supported if we got no caps. |
|---|
| 207 | # This is the "Asterix way", after 0.12 release, I will |
|---|
| 208 | # likely implement a fallback to disco (could be disabled |
|---|
| 209 | # for mobile users who pay for traffic) |
|---|
| 210 | if contact.caps_hash_method == 'old': |
|---|
| 211 | features = self[(contact.caps_hash_method, contact.caps_node + '#' + \ |
|---|
| 212 | contact.caps_hash)].features |
|---|
| 213 | else: |
|---|
| 214 | features = self[(contact.caps_hash_method, contact.caps_hash)].features |
|---|
| 215 | if feature in features or features == []: |
|---|
| 216 | return True |
|---|
| 217 | |
|---|
| 218 | return False |
|---|
| 219 | |
|---|
| 220 | gajim.capscache = CapsCache(gajim.logger) |
|---|
| 221 | |
|---|
| 222 | class ConnectionCaps(object): |
|---|
| 223 | ''' This class highly depends on that it is a part of Connection class. ''' |
|---|
| 224 | def _capsPresenceCB(self, con, presence): |
|---|
| 225 | ''' Handle incoming presence stanzas... This is a callback |
|---|
| 226 | for xmpp registered in connection_handlers.py''' |
|---|
| 227 | |
|---|
| 228 | # we will put these into proper Contact object and ask |
|---|
| 229 | # for disco... so that disco will learn how to interpret |
|---|
| 230 | # these caps |
|---|
| 231 | pm_ctrl = None |
|---|
| 232 | jid = helpers.get_full_jid_from_iq(presence) |
|---|
| 233 | contact = gajim.contacts.get_contact_from_full_jid(self.name, jid) |
|---|
| 234 | if contact is None: |
|---|
| 235 | room_jid, nick = gajim.get_room_and_nick_from_fjid(jid) |
|---|
| 236 | contact = gajim.contacts.get_gc_contact( |
|---|
| 237 | self.name, room_jid, nick) |
|---|
| 238 | pm_ctrl = gajim.interface.msg_win_mgr.get_control(jid, self.name) |
|---|
| 239 | if contact is None: |
|---|
| 240 | # TODO: a way to put contact not-in-roster |
|---|
| 241 | # into Contacts |
|---|
| 242 | return |
|---|
| 243 | |
|---|
| 244 | # get the caps element |
|---|
| 245 | caps = presence.getTag('c') |
|---|
| 246 | if not caps: |
|---|
| 247 | contact.caps_node = None |
|---|
| 248 | contact.caps_hash = None |
|---|
| 249 | contact.caps_hash_method = None |
|---|
| 250 | return |
|---|
| 251 | |
|---|
| 252 | hash_method, node, hash_ = caps['hash'], caps['node'], caps['ver'] |
|---|
| 253 | |
|---|
| 254 | if hash_method is None and node and hash_: |
|---|
| 255 | # Old XEP-115 implentation |
|---|
| 256 | hash_method = 'old' |
|---|
| 257 | |
|---|
| 258 | if hash_method is None or node is None or hash_ is None: |
|---|
| 259 | # improper caps in stanza, ignoring |
|---|
| 260 | contact.caps_node = None |
|---|
| 261 | contact.caps_hash = None |
|---|
| 262 | contact.hash_method = None |
|---|
| 263 | return |
|---|
| 264 | |
|---|
| 265 | # start disco query... |
|---|
| 266 | gajim.capscache.preload(self, jid, node, hash_method, hash_) |
|---|
| 267 | |
|---|
| 268 | # overwriting old data |
|---|
| 269 | contact.caps_node = node |
|---|
| 270 | contact.caps_hash_method = hash_method |
|---|
| 271 | contact.caps_hash = hash_ |
|---|
| 272 | if pm_ctrl: |
|---|
| 273 | pm_ctrl.update_contact() |
|---|
| 274 | |
|---|
| 275 | def _capsDiscoCB(self, jid, node, identities, features, dataforms): |
|---|
| 276 | contact = gajim.contacts.get_contact_from_full_jid(self.name, jid) |
|---|
| 277 | if not contact: |
|---|
| 278 | room_jid, nick = gajim.get_room_and_nick_from_fjid(jid) |
|---|
| 279 | contact = gajim.contacts.get_gc_contact(self.name, room_jid, nick) |
|---|
| 280 | if contact is None: |
|---|
| 281 | return |
|---|
| 282 | if not contact.caps_node: |
|---|
| 283 | return # we didn't asked for that? |
|---|
| 284 | if contact.caps_hash_method != 'old': |
|---|
| 285 | computed_hash = helpers.compute_caps_hash(identities, features, |
|---|
| 286 | dataforms=dataforms, hash_method=contact.caps_hash_method) |
|---|
| 287 | if computed_hash != contact.caps_hash: |
|---|
| 288 | # wrong hash, forget it |
|---|
| 289 | contact.caps_node = '' |
|---|
| 290 | contact.caps_hash_method = '' |
|---|
| 291 | contact.caps_hash = '' |
|---|
| 292 | return |
|---|
| 293 | # if we don't have this info already... |
|---|
| 294 | caps = gajim.capscache[(contact.caps_hash_method, contact.caps_hash)] |
|---|
| 295 | else: |
|---|
| 296 | # if we don't have this info already... |
|---|
| 297 | caps = gajim.capscache[(contact.caps_hash_method, contact.caps_node + \ |
|---|
| 298 | '#' + contact.caps_hash)] |
|---|
| 299 | if caps.queried == 2: |
|---|
| 300 | return |
|---|
| 301 | |
|---|
| 302 | caps.update(identities, features) |
|---|
| 303 | |
|---|
| 304 | # vim: se ts=3: |
|---|