gajim-plural/gajim/common/modules/discovery.py

301 lines
11 KiB
Python

# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0030: Service Discovery
import logging
import weakref
import nbxmpp
from gajim.common import app
from gajim.common import helpers
from gajim.common.caps_cache import muc_caps_cache
from gajim.common.nec import NetworkIncomingEvent
from gajim.common.connection_handlers_events import InformationEvent
log = logging.getLogger('gajim.c.m.discovery')
class Discovery:
def __init__(self, con):
self._con = con
self._account = con.name
self.handlers = [
('iq', self._answer_disco_info, 'get', nbxmpp.NS_DISCO_INFO),
('iq', self._answer_disco_items, 'get', nbxmpp.NS_DISCO_ITEMS),
]
def disco_contact(self, jid, node=None):
success_cb = self._con.get_module('Caps').contact_info_received
self._disco(nbxmpp.NS_DISCO_INFO, jid, node, success_cb, None)
def disco_items(self, jid, node=None, success_cb=None, error_cb=None):
self._disco(nbxmpp.NS_DISCO_ITEMS, jid, node, success_cb, error_cb)
def disco_info(self, jid, node=None, success_cb=None, error_cb=None):
self._disco(nbxmpp.NS_DISCO_INFO, jid, node, success_cb, error_cb)
def _disco(self, namespace, jid, node, success_cb, error_cb):
if success_cb is None:
raise ValueError('success_cb is required')
if not app.account_is_connected(self._account):
return
iq = nbxmpp.Iq(typ='get', to=jid, queryNS=namespace)
if node:
iq.setQuerynode(node)
log_str = 'Request info: %s %s'
if namespace == nbxmpp.NS_DISCO_ITEMS:
log_str = 'Request items: %s %s'
log.info(log_str, jid, node or '')
# Create weak references so we can pass GUI object methods
weak_success_cb = weakref.WeakMethod(success_cb)
if error_cb is not None:
weak_error_cb = weakref.WeakMethod(error_cb)
else:
weak_error_cb = None
self._con.connection.SendAndCallForResponse(
iq, self._disco_response, {'success_cb': weak_success_cb,
'error_cb': weak_error_cb})
def _disco_response(self, conn, stanza, success_cb, error_cb):
if not nbxmpp.isResultNode(stanza):
if error_cb is not None:
error_cb()(stanza.getFrom(), stanza.getError())
else:
log.info('Error: %s', stanza.getError())
return
from_ = stanza.getFrom()
node = stanza.getQuerynode()
if stanza.getQueryNS() == nbxmpp.NS_DISCO_INFO:
identities, features, data, node = self.parse_info_response(stanza)
success_cb()(from_, identities, features, data, node)
elif stanza.getQueryNS() == nbxmpp.NS_DISCO_ITEMS:
items = self.parse_items_response(stanza)
success_cb()(from_, node, items)
else:
log.warning('Wrong query namespace: %s', stanza)
@classmethod
def parse_items_response(cls, stanza):
payload = stanza.getQueryPayload()
items = []
for item in payload:
# CDATA payload is not processed, only nodes
if not isinstance(item, nbxmpp.simplexml.Node):
continue
attr = item.getAttrs()
if 'jid' not in attr:
log.warning('No jid attr in disco items: %s', stanza)
continue
try:
attr['jid'] = helpers.parse_jid(attr['jid'])
except helpers.InvalidFormat:
log.warning('Invalid jid attr in disco items: %s', stanza)
continue
items.append(attr)
return items
@classmethod
def parse_info_response(cls, stanza):
identities, features, data, node = [], [], [], None
q = stanza.getTag('query')
node = q.getAttr('node')
if not node:
node = ''
qc = stanza.getQueryChildren()
if not qc:
qc = []
for i in qc:
if i.getName() == 'identity':
attr = {}
for key in i.getAttrs().keys():
attr[key] = i.getAttr(key)
identities.append(attr)
elif i.getName() == 'feature':
var = i.getAttr('var')
if var:
features.append(var)
elif i.getName() == 'x' and i.getNamespace() == nbxmpp.NS_DATA:
data.append(nbxmpp.DataForm(node=i))
return identities, features, data, node
def discover_server_items(self):
server = self._con.get_own_jid().getDomain()
self.disco_items(server, success_cb=self._server_items_received)
def _server_items_received(self, from_, node, items):
log.info('Server items received')
for item in items:
if 'node' in item:
# Only disco components
continue
self.disco_info(item['jid'],
success_cb=self._server_items_info_received)
def _server_items_info_received(self, from_, *args):
from_ = from_.getStripped()
log.info('Server item info received: %s', from_)
self._parse_transports(from_, *args)
try:
self._con.get_module('MUC').pass_disco(from_, *args)
self._con.get_module('HTTPUpload').pass_disco(from_, *args)
self._con.pass_bytestream_disco(from_, *args)
except nbxmpp.NodeProcessed:
pass
app.nec.push_incoming_event(
NetworkIncomingEvent('server-disco-received'))
def discover_account_info(self):
own_jid = self._con.get_own_jid().getStripped()
self.disco_info(own_jid, success_cb=self._account_info_received)
def _account_info_received(self, from_, *args):
from_ = from_.getStripped()
log.info('Account info received: %s', from_)
self._con.get_module('MAM').pass_disco(from_, *args)
self._con.get_module('PEP').pass_disco(from_, *args)
self._con.get_module('PubSub').pass_disco(from_, *args)
def discover_server_info(self):
# Calling this method starts the connect_maschine()
server = self._con.get_own_jid().getDomain()
self.disco_info(server, success_cb=self._server_info_received)
def _server_info_received(self, from_, *args):
log.info('Server info received: %s', from_)
self._con.get_module('SecLabels').pass_disco(from_, *args)
self._con.get_module('Blocking').pass_disco(from_, *args)
self._con.get_module('VCardTemp').pass_disco(from_, *args)
self._con.get_module('Carbons').pass_disco(from_, *args)
self._con.get_module('PrivacyLists').pass_disco(from_, *args)
self._con.get_module('HTTPUpload').pass_disco(from_, *args)
identities, features, data, node = args
if nbxmpp.NS_REGISTER in features:
self._con.register_supported = True
if nbxmpp.NS_ADDRESS in features:
self._con.addressing_supported = True
self._con.connect_machine(restart=True)
def _parse_transports(self, from_, identities, features, data, node):
for identity in identities:
category = identity.get('category')
if category not in ('gateway', 'headline'):
continue
transport_type = identity.get('type')
log.info('Found transport: %s %s %s',
from_, category, transport_type)
jid = str(from_)
if jid not in app.transport_type:
app.transport_type[jid] = transport_type
app.logger.save_transport_type(jid, transport_type)
if transport_type in self._con.available_transports:
self._con.available_transports[transport_type].append(jid)
else:
self._con.available_transports[transport_type] = [jid]
def _answer_disco_items(self, con, stanza):
from_ = stanza.getFrom()
log.info('Answer disco items to %s', from_)
if self._con.get_module('AdHocCommands').command_items_query(stanza):
raise nbxmpp.NodeProcessed
node = stanza.getTagAttr('query', 'node')
if node is None:
result = stanza.buildReply('result')
self._con.connection.send(result)
raise nbxmpp.NodeProcessed
if node == nbxmpp.NS_COMMANDS:
self._con.get_module('AdHocCommands').command_list_query(stanza)
raise nbxmpp.NodeProcessed
def _answer_disco_info(self, con, stanza):
from_ = stanza.getFrom()
log.info('Answer disco info %s', from_)
if str(from_).startswith('echo.'):
# Service that echos all stanzas, ignore it
raise nbxmpp.NodeProcessed
if self._con.get_module('AdHocCommands').command_info_query(stanza):
raise nbxmpp.NodeProcessed
node = stanza.getQuerynode()
iq = stanza.buildReply('result')
query = iq.setQuery()
if node:
query.setAttr('node', node)
query.addChild('identity', attrs=app.gajim_identity)
client_version = 'http://gajim.org#' + app.caps_hash[self._account]
if node in (None, client_version):
for f in app.gajim_common_features:
query.addChild('feature', attrs={'var': f})
for f in app.gajim_optional_features[self._account]:
query.addChild('feature', attrs={'var': f})
self._con.connection.send(iq)
raise nbxmpp.NodeProcessed
def disco_muc(self, jid, callback, update=False):
if not app.account_is_connected(self._account):
return
if muc_caps_cache.is_cached(jid) and not update:
callback()
return
iq = nbxmpp.Iq(typ='get', to=jid, queryNS=nbxmpp.NS_DISCO_INFO)
log.info('Request MUC info %s', jid)
self._con.connection.SendAndCallForResponse(
iq, self._muc_info_response, {'callback': callback})
def _muc_info_response(self, conn, stanza, callback):
if not nbxmpp.isResultNode(stanza):
error = stanza.getError()
if error == 'item-not-found':
# Groupchat does not exist
log.info('MUC does not exist: %s', stanza.getFrom())
callback()
else:
log.info('MUC disco error: %s', error)
app.nec.push_incoming_event(
InformationEvent(
None, dialog_name='unable-join-groupchat', args=error))
return
log.info('MUC info received: %s', stanza.getFrom())
muc_caps_cache.append(stanza)
callback()
def get_instance(*args, **kwargs):
return Discovery(*args, **kwargs), 'Discovery'