Commit 9c53fcbd authored by Philipp Hörist's avatar Philipp Hörist

Use nbxmpp's Discovery module

parent 4757fd95
Pipeline #3794 passed with stages
in 2 minutes and 47 seconds
......@@ -38,6 +38,7 @@ from pathlib import Path
from collections import namedtuple
import nbxmpp
from nbxmpp.structs import DiscoIdentity
from gi.repository import Gdk
import gajim
......@@ -143,7 +144,9 @@ socks5queue = None
gupnp_igd = None
gajim_identity = {'type': 'pc', 'category': 'client', 'name': 'Gajim'}
gajim_identity = DiscoIdentity(category='client',
type='pc',
name='Gajim')
gajim_common_features = [
nbxmpp.NS_BYTESTREAM,
......
......@@ -30,7 +30,6 @@ through ClientCaps objects which are hold by contact instances.
import base64
import hashlib
import logging
from collections import namedtuple
import nbxmpp
from nbxmpp.const import Affiliation
......@@ -114,19 +113,18 @@ def compute_caps_hash(identities, features, dataforms=None, hash_method='sha-1')
dataforms = []
def sort_identities_key(i):
return (i['category'], i.get('type', ''), i.get('xml:lang', ''))
return (i.category, i.type, i.lang or '')
def sort_dataforms_key(dataform):
f = dataform.getField('FORM_TYPE')
return (bool(f), f.getValue())
return dataform['FORM_TYPE'].value
S = ''
identities.sort(key=sort_identities_key)
for i in identities:
c = i['category']
type_ = i.get('type', '')
lang = i.get('xml:lang', '')
name = i.get('name', '')
c = i.category
type_ = i.type
lang = i.lang or ''
name = i.name or ''
S += '%s/%s/%s/%s<' % (c, type_, lang, name)
features.sort()
for f in features:
......@@ -135,15 +133,16 @@ def compute_caps_hash(identities, features, dataforms=None, hash_method='sha-1')
for dataform in dataforms:
# fields indexed by var
fields = {}
for f in dataform.getChildren():
fields[f.getVar()] = f
for f in dataform.iter_fields():
values = f.getTags('value')
fields[f.var] = [value.getData() for value in values]
form_type = fields.get('FORM_TYPE')
if form_type:
S += form_type.getValue() + '<'
S += form_type[0] + '<'
del fields['FORM_TYPE']
for var in sorted(fields.keys()):
S += '%s<' % var
values = sorted(fields[var].getValues())
values = sorted(fields[var])
for value in values:
S += '%s<' % value
......@@ -331,27 +330,10 @@ class CapsCache:
features = property(_get_features, _set_features)
def _get_identities(self):
list_ = []
for i in self._identities:
# transforms it back in a dict
d = dict()
d['category'] = i[0]
if i[1]:
d['type'] = i[1]
if i[2]:
d['xml:lang'] = i[2]
if i[3]:
d['name'] = i[3]
list_.append(d)
return list_
return self._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'))
self._identities.append(self.__names.setdefault(t, t))
self._identities = value
identities = property(_get_identities, _set_identities)
......@@ -432,37 +414,15 @@ class CapsCache:
class MucCapsCache:
DiscoInfo = namedtuple('DiscoInfo', ['identities', 'features', 'data'])
def __init__(self):
self.cache = {}
def append(self, stanza):
jid = stanza.getFrom()
identities, features, data = [], [], []
query_childs = stanza.getQueryChildren()
if not query_childs:
log.warning('%s returned empty disco info', jid)
return
for child in query_childs:
if child.getName() == 'identity':
attr = {}
for key in child.getAttrs().keys():
attr[key] = child.getAttr(key)
identities.append(attr)
elif child.getName() == 'feature':
features.append(child.getAttr('var'))
elif child.getName() == 'x':
if child.getNamespace() == nbxmpp.NS_DATA:
from gajim.common.modules import dataforms
data.append(dataforms.extend_form(node=child))
if nbxmpp.NS_MUC not in features:
def append(self, info):
if nbxmpp.NS_MUC not in info.features:
# Not a MUC, don't cache info
return
self.cache[jid] = self.DiscoInfo(identities, features, data)
self.cache[info.jid] = info
def is_cached(self, jid):
return jid in self.cache
......@@ -498,7 +458,7 @@ class MucCapsCache:
return allowed
if jid in self.cache:
for form in self.cache[jid].data:
for form in self.cache[jid].dataforms:
try:
allowed = form['muc#roominfo_changesubject'].value
except KeyError:
......@@ -520,7 +480,7 @@ class MucCapsCache:
def get_room_infos(self, jid):
room_info = {}
if jid in self.cache:
for form in self.cache[jid].data:
for form in self.cache[jid].dataforms:
try:
room_info['name'] = form['muc#roomconfig_roomname'].value
except KeyError:
......
......@@ -38,6 +38,8 @@ from gzip import GzipFile
from io import BytesIO
from gi.repository import GLib
from nbxmpp.structs import DiscoIdentity
from gajim.common import exceptions
from gajim.common import app
from gajim.common import configpaths
......@@ -1027,8 +1029,10 @@ class Logger:
type_ = data[i + 1]
lang = data[i + 2]
name = data[i + 3]
identities.append({'category': category, 'type': type_,
'xml:lang': lang, 'name': name})
identities.append(DiscoIdentity(category=category,
type=type_,
lang=lang,
name=name))
i += 4
i += 1
while i < len(data):
......@@ -1046,10 +1050,12 @@ class Logger:
data = []
for identity in identities:
# there is no FEAT category
if identity['category'] == 'FEAT':
if identity.category == 'FEAT':
return
data.extend((identity.get('category'), identity.get('type', ''),
identity.get('xml:lang', ''), identity.get('name', '')))
data.extend((identity.category,
identity.type,
identity.lang or '',
identity.name or ''))
data.append('FEAT')
data.extend(features)
data = '\0'.join(data)
......
......@@ -50,8 +50,8 @@ class Blocking(BaseModule):
self.supported = False
def pass_disco(self, from_, _identities, features, _data, _node):
if nbxmpp.NS_BLOCKING not in features:
def pass_disco(self, info):
if nbxmpp.NS_BLOCKING not in info.features:
return
self.supported = True
......@@ -60,7 +60,7 @@ class Blocking(BaseModule):
account=self._account,
feature=nbxmpp.NS_BLOCKING))
self._log.info('Discovered blocking: %s', from_)
self._log.info('Discovered blocking: %s', info.jid)
def _blocking_list_received(self, result):
if is_error_result(result):
......
......@@ -82,11 +82,11 @@ class Bookmarks(BaseModule):
app.nec.push_incoming_event(
NetworkEvent('bookmarks-received', account=self._account))
def pass_disco(self, from_, _identities, features, _data, _node):
if nbxmpp.NS_BOOKMARK_CONVERSION not in features:
def pass_disco(self, info):
if nbxmpp.NS_BOOKMARK_CONVERSION not in info.features:
return
self._conversion = True
self._log.info('Discovered Bookmarks Conversion: %s', from_)
self._log.info('Discovered Bookmarks Conversion: %s', info.jid)
def _act_on_changed_bookmarks(self, old_bookmarks):
new_bookmarks = self._convert_to_set(self._bookmarks)
......
......@@ -95,16 +95,16 @@ class Bytestream(BaseModule):
callback=self._ResultCB),
]
def pass_disco(self, from_, _identities, features, _data, _node):
if nbxmpp.NS_BYTESTREAM not in features:
def pass_disco(self, info):
if nbxmpp.NS_BYTESTREAM not in info.features:
return
if app.config.get_per('accounts', self._account, 'use_ft_proxies'):
log.info('Discovered proxy: %s', from_)
log.info('Discovered proxy: %s', info.jid)
our_fjid = self._con.get_own_jid()
testit = app.config.get_per(
'accounts', self._account, 'test_ft_proxies_on_startup')
app.proxy65_manager.resolve(
from_, self._con.connection, str(our_fjid),
info.jid, self._con.connection, str(our_fjid),
default=self._account, testit=testit)
raise nbxmpp.NodeProcessed
......
......@@ -19,6 +19,7 @@
import nbxmpp
from nbxmpp.structs import StanzaHandler
from nbxmpp.util import is_error_result
from gajim.common import caps_cache
from gajim.common import app
......@@ -94,16 +95,21 @@ class Caps(BaseModule):
self._account, room_jid, resource)
return contact
def contact_info_received(self, from_, identities, features, data, node):
def contact_info_received(self, info):
"""
callback to update our caps cache with queried information after
we have retrieved an unknown caps hash via a disco
"""
bare_jid = from_.getStripped()
contact = self._get_contact_or_gc_contact_for_jid(from_)
if is_error_result(info):
self._log.info(info)
return
bare_jid = info.jid.getBare()
contact = self._get_contact_or_gc_contact_for_jid(info.jid)
if not contact:
self._log.info('Received Disco from unknown contact %s', from_)
self._log.info('Received Disco from unknown contact %s', info.jid)
return
lookup = contact.client_caps.get_cache_lookup_strategy()
......@@ -115,10 +121,10 @@ class Caps(BaseModule):
return
validate = contact.client_caps.get_hash_validation_strategy()
hash_is_valid = validate(identities, features, data)
hash_is_valid = validate(info.identities, info.features, info.dataforms)
if hash_is_valid:
cache_item.set_and_store(identities, features)
cache_item.set_and_store(info.identities, info.features)
else:
node = caps_hash = hash_method = None
contact.client_caps = self._create_suitable_client_caps(
......@@ -126,11 +132,10 @@ class Caps(BaseModule):
self._log.warning(
'Computed and retrieved caps hash differ. Ignoring '
'caps of contact %s', contact.get_full_jid())
app.nec.push_incoming_event(
NetworkEvent('caps-update',
conn=self._con,
fjid=str(from_),
fjid=str(info.jid),
jid=bare_jid))
......
......@@ -25,12 +25,12 @@ class Carbons(BaseModule):
self.supported = False
def pass_disco(self, from_, _identities, features, _data, _node):
if nbxmpp.NS_CARBONS not in features:
def pass_disco(self, info):
if nbxmpp.NS_CARBONS not in info.features:
return
self.supported = True
self._log.info('Discovered carbons: %s', from_)
self._log.info('Discovered carbons: %s', info.jid)
iq = nbxmpp.Iq('set')
iq.setTag('enable', namespace=nbxmpp.NS_CARBONS)
......
This diff is collapsed.
......@@ -71,22 +71,22 @@ class HTTPUpload(BaseModule):
ged.OUT_PREGUI,
self.handle_outgoing_stanza)
def pass_disco(self, from_, _identities, features, data, _node):
if NS_HTTPUPLOAD_0 in features:
def pass_disco(self, info):
if NS_HTTPUPLOAD_0 in info.features:
self.httpupload_namespace = NS_HTTPUPLOAD_0
elif NS_HTTPUPLOAD in features:
elif NS_HTTPUPLOAD in info.features:
self.httpupload_namespace = NS_HTTPUPLOAD
else:
return
self.component = from_
self._log.info('Discovered component: %s', from_)
self.component = info.jid
self._log.info('Discovered component: %s', info.jid)
for form in data:
for form in info.dataforms:
form_dict = form.asDict()
if form_dict.get('FORM_TYPE', None) != self.httpupload_namespace:
if form_dict.get('FORM_TYPE') != self.httpupload_namespace:
continue
size = form_dict.get('max-file-size', None)
size = form_dict.get('max-file-size')
if size is not None:
self.max_file_size = int(size)
break
......
......@@ -52,16 +52,17 @@ class MAM(BaseModule):
# Holds archive jids where catch up was successful
self._catch_up_finished = []
def pass_disco(self, from_, _identities, features, _data, _node):
if nbxmpp.NS_MAM_2 in features:
def pass_disco(self, info):
if nbxmpp.NS_MAM_2 in info.features:
self.archiving_namespace = nbxmpp.NS_MAM_2
elif nbxmpp.NS_MAM_1 in features:
elif nbxmpp.NS_MAM_1 in info.features:
self.archiving_namespace = nbxmpp.NS_MAM_1
else:
return
self.available = True
self._log.info('Discovered MAM %s: %s', self.archiving_namespace, from_)
self._log.info('Discovered MAM %s: %s',
self.archiving_namespace, info.jid)
app.nec.push_incoming_event(
NetworkEvent('feature-discovered',
......
......@@ -103,16 +103,16 @@ class MUC(BaseModule):
self._muc_data = {}
def pass_disco(self, from_, identities, features, _data, _node):
for identity in identities:
if identity.get('category') != 'conference':
def pass_disco(self, info):
for identity in info.identities:
if identity.category != 'conference':
continue
if identity.get('type') != 'text':
if identity.type != 'text':
continue
if nbxmpp.NS_MUC in features:
self._log.info('Discovered MUC: %s', from_)
if nbxmpp.NS_MUC in info.features:
self._log.info('Discovered MUC: %s', info.jid)
# TODO: make this nicer
self._con.muc_jid['jabber'] = from_
self._con.muc_jid['jabber'] = str(info.jid)
raise nbxmpp.NodeProcessed
def _get_muc_data(self, room_jid):
......
......@@ -15,12 +15,8 @@
# XEP-0163: Personal Eventing Protocol
from typing import Any
from typing import Dict
from typing import List
from typing import Tuple
import nbxmpp
from gajim.common.types import ConnectionT
from gajim.common.modules.base import BaseModule
......@@ -31,16 +27,11 @@ class PEP(BaseModule):
self.supported = False
def pass_disco(self,
from_: nbxmpp.JID,
identities: List[Dict[str, str]],
_features: List[str],
_data: List[nbxmpp.DataForm],
_node: str) -> None:
for identity in identities:
if identity['category'] == 'pubsub':
if identity.get('type') == 'pep':
self._log.info('Discovered PEP support: %s', from_)
def pass_disco(self, info):
for identity in info.identities:
if identity.category == 'pubsub':
if identity.type == 'pep':
self._log.info('Discovered PEP support: %s', info.jid)
self.supported = True
......
......@@ -45,12 +45,12 @@ class PrivacyLists(BaseModule):
self.supported = False
def pass_disco(self, from_, _identities, features, _data, _node):
if nbxmpp.NS_PRIVACY not in features:
def pass_disco(self, info):
if nbxmpp.NS_PRIVACY not in info.features:
return
self.supported = True
self._log.info('Discovered XEP-0016: Privacy Lists: %s', from_)
self._log.info('Discovered XEP-0016: Privacy Lists: %s', info.jid)
app.nec.push_incoming_event(
NetworkEvent('feature-discovered',
......
......@@ -34,12 +34,12 @@ class PubSub(BaseModule):
self.publish_options = False
def pass_disco(self, from_, _identities, features, _data, _node):
if nbxmpp.NS_PUBSUB_PUBLISH_OPTIONS not in features:
def pass_disco(self, info):
if nbxmpp.NS_PUBSUB_PUBLISH_OPTIONS not in info.features:
# Remove stored bookmarks accessible to everyone.
self._con.get_module('Bookmarks').purge_pubsub_bookmarks()
return
self._log.info('Discovered Pubsub publish options: %s', from_)
self._log.info('Discovered Pubsub publish options: %s', info.jid)
self.publish_options = True
def send_pb_subscription_query(self, jid, cb, **kwargs):
......
......@@ -28,12 +28,12 @@ class SecLabels(BaseModule):
self._catalogs = {}
self.supported = False
def pass_disco(self, from_, _identities, features, _data, _node):
if nbxmpp.NS_SECLABEL not in features:
def pass_disco(self, info):
if nbxmpp.NS_SECLABEL not in info.features:
return
self.supported = True
self._log.info('Discovered security labels: %s', from_)
self._log.info('Discovered security labels: %s', info.jid)
def request_catalog(self, jid):
server = app.get_jid_from_account(self._account).split("@")[1]
......
......@@ -36,12 +36,12 @@ class VCardTemp(BaseModule):
self.room_jids = []
self.supported = False
def pass_disco(self, from_, _identities, features, _data, _node):
if nbxmpp.NS_VCARD not in features:
def pass_disco(self, info):
if nbxmpp.NS_VCARD not in info.features:
return
self.supported = True
self._log.info('Discovered vcard-temp: %s', from_)
self._log.info('Discovered vcard-temp: %s', info.jid)
app.nec.push_incoming_event(NetworkEvent('feature-discovered',
account=self._account,
......
This diff is collapsed.
......@@ -12,10 +12,14 @@
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import nbxmpp
from gi.repository import Gtk
from gi.repository import Gdk
import nbxmpp
from nbxmpp.util import is_error_result
from nbxmpp.structs import DiscoIdentity
from gajim.common import app
from gajim.common import helpers
from gajim.common.i18n import _
......@@ -272,23 +276,26 @@ class JoinGroupchatWindow(Gtk.ApplicationWindow):
server = self.server_combo.get_active_text().strip()
con = app.connections[self.account]
con.get_module('Discovery').disco_info(
server,
success_cb=self._disco_info_received,
error_cb=self._disco_info_error)
server, callback=self._disco_info_received)
def _disco_info_error(self, from_, error):
ErrorDialog(_('Wrong server'),
_('%s is not a groupchat server') % from_,
transient_for=self)
def _disco_info_received(self, from_, identities, features, data, node):
if nbxmpp.NS_MUC not in features:
def _disco_info_received(self, result):
if is_error_result(result):
ErrorDialog(_('Wrong server'),
_('%s is not a groupchat server') % result.jid,
transient_for=self)
return
if nbxmpp.NS_MUC not in result.features:
ErrorDialog(_('Wrong server'),
_('%s is not a groupchat server') % from_,
_('%s is not a groupchat server') % result.jid,
transient_for=self)
return
jid = str(from_)
jid = str(result.jid)
if jid in app.interface.instances[self.account]['disco']:
app.interface.instances[self.account]['disco'][jid].window.\
present()
......@@ -297,7 +304,8 @@ class JoinGroupchatWindow(Gtk.ApplicationWindow):
# Object will add itself to the window dict
ServiceDiscoveryWindow(
self.account, jid,
initial_identities=[{'category': 'conference',
'type': 'text'}])
initial_identities=[DiscoIdentity(category='conference',
type='text',
name=None)])
except GajimGeneralException:
pass
......@@ -14,6 +14,7 @@
from gi.repository import Gdk
from gi.repository import Gtk
from nbxmpp.util import is_error_result
from gajim.common import app
from gajim.common import ged
......@@ -83,18 +84,17 @@ class ManagePEPServicesWindow(Gtk.ApplicationWindow):
jid = self._con.get_own_jid().getStripped()
self._con.get_module('Discovery').disco_items(
jid,
success_cb=self._items_received,
error_cb=self._items_error)
jid, callback=self._items_received)
def _items_received(self, from_, node, items):
jid = self._con.get_own_jid().getStripped()
for item in items:
if item['jid'] == jid and 'node' in item:
self.treestore.append([item['node']])
def _items_received(self, result):
if is_error_result(result):
ErrorDialog('Error', str(result))
return
def _items_error(self, from_, error):
ErrorDialog('Error', error)
jid = result.jid.getBare()
for item in result.items:
if item.jid == jid and item.node is not None:
self.treestore.append([item.node])
def _node_removed(self, jid, node):
if jid != app.get_jid_from_account(self.account):
......
......@@ -2,14 +2,16 @@
Tests for capabilities and the capabilities cache
'''
import unittest
import weakref
from unittest.mock import MagicMock, Mock
from nbxmpp import NS_MUC, NS_PING, NS_XHTML_IM, Iq
from nbxmpp.structs import DiscoIdentity
from nbxmpp.modules.discovery import Discovery
from gajim.common import caps_cache as caps
from gajim.common.modules.discovery import Discovery
EXAMPLES = [
'''<iq>
'''<iq type='result'>
<query xmlns='http://jabber.org/protocol/disco#info' node='http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w='>
<identity xml:lang='en' category='client' name='Psi 0.11' type='pc'/>
<identity xml:lang='el' category='client' name='Ψ 0.11' type='pc'/>
......@@ -42,7 +44,7 @@ EXAMPLES = [
</iq>
''',
'''<iq>
'''<iq type='result'>
<query node="http://bombusmod.net.ru/caps#tbBQGBMv8g8U7kW55TEZZRnMCJ4=" xmlns="http://jabber.org/protocol/disco#info">
<identity category="client" name="BombusMod" type="mobile"/>
<feature var="http://jabber.org/protocol/chatstates"/>
......@@ -62,7 +64,7 @@ EXAMPLES = [
</iq>
''',
'''<iq>
'''<iq type='result'>
<query node="http://jappix.org/#qRsaGbKTz8EwAOakYO00InkZUxM=" xmlns="http://jabber.org/protocol/disco#info">
<identity category="client" name="Jappix" type="web"/>
<feature var="http://jabber.org/protocol/activity"/>
......@@ -137,10 +139,12 @@ class CommonCapsTest(unittest.TestCase):
self.client_caps = (self.caps_method, self.caps_hash)
self.node = "http://gajim.org"
self.identity = {'category': 'client', 'type': 'pc', 'name':'Gajim'}
self.identity = DiscoIdentity(category='client',
type='pc',
name='Gajim')
self.identities = [self.identity]
self.features = [NS_MUC, NS_XHTML_IM] # NS_MUC not supported!
self.features = [NS_MUC, NS_XHTML_IM]
# Simulate a filled db
db_caps_cache = [
......@@ -169,8 +173,8 @@ class TestCapsCache(CommonCapsTest):
self.assertEqual(1, len(identities))
identity = identities[0]
self.assertEqual('client', identity['category'])
self.assertEqual('pc', identity['type'])
self.assertEqual('client', identity.category)
self.assertEqual('pc', identity.type)
def test_set_and_store(self):
''' Test client_caps update gets logged into db '''
......@@ -214,10 +218,14 @@ class TestCapsCache(CommonCapsTest):
'''tests the hash computation'''
for example in EXAMPLES:
stanza = Iq(node=example)
identities, features, data, node = Discovery.parse_info_response(stanza)
computed_hash = caps.compute_caps_hash(identities, features, data)
hash_ = node.split('#')[1]
self.assertEqual(hash_, computed_hash)
disco_module = Discovery(None)
weak_callback = weakref.WeakMethod(self.disco_info_received)
disco_module._disco_info_received(None, stanza, callback=weak_callback)
def disco_info_received(self, info):
computed_hash = caps.compute_caps_hash(info.identities, info.features, info.dataforms)
hash_ = info.node.split('#')[1]
self.assertEqual(hash_, computed_hash)
class TestClientCaps(CommonCapsTest):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment