gajim3/gajim/common/modules/caps.py

233 lines
7.3 KiB
Python

# Copyright (C) 2009 Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com>
#
# 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-0115: Entity Capabilities
import weakref
from collections import defaultdict
from nbxmpp.namespaces import Namespace
from nbxmpp.structs import StanzaHandler
from nbxmpp.structs import DiscoIdentity
from nbxmpp.util import compute_caps_hash
from nbxmpp.errors import StanzaError
from gajim.common import app
from gajim.common.const import COMMON_FEATURES
from gajim.common.const import Entity
from gajim.common.helpers import get_optional_features
from gajim.common.nec import NetworkEvent
from gajim.common.task_manager import Task
from gajim.common.modules.base import BaseModule
class Caps(BaseModule):
_nbxmpp_extends = 'EntityCaps'
_nbxmpp_methods = [
'caps',
'set_caps'
]
def __init__(self, con):
BaseModule.__init__(self, con)
self.handlers = [
StanzaHandler(name='presence',
callback=self._entity_caps,
ns=Namespace.CAPS,
priority=51),
]
self._identities = [
DiscoIdentity(category='client', type='pc', name='Gajim')
]
self._queued_tasks_by_hash = defaultdict(set)
self._queued_tasks_by_jid = {}
def _queue_task(self, task):
old_task = self._get_task(task.entity.jid)
if old_task is not None:
self._remove_task(old_task)
self._log.info('Queue query for hash %s', task.entity.hash)
self._queued_tasks_by_hash[task.entity.hash].add(task)
self._queued_tasks_by_jid[task.entity.jid] = task
app.task_manager.add_task(task)
def _get_task(self, jid):
return self._queued_tasks_by_jid.get(jid)
def _get_similar_tasks(self, task):
return self._queued_tasks_by_hash.pop(task.entity.hash)
def _remove_task(self, task):
task.set_obsolete()
del self._queued_tasks_by_jid[task.entity.jid]
self._queued_tasks_by_hash[task.entity.hash].discard(task)
def _remove_all_tasks(self):
for task in self._queued_tasks_by_jid.values():
task.set_obsolete()
self._queued_tasks_by_jid.clear()
self._queued_tasks_by_hash.clear()
def _entity_caps(self, _con, _stanza, properties):
if properties.type.is_error or properties.type.is_unavailable:
return
if properties.is_self_presence:
return
if properties.entity_caps is None:
return
task = EntityCapsTask(self._account, properties, self._execute_task)
self._log.info('Received %s', task.entity)
disco_info = app.storage.cache.get_caps_entry(task.entity.method,
task.entity.hash)
if disco_info is None:
self._queue_task(task)
return
jid = str(properties.jid)
app.storage.cache.set_last_disco_info(jid, disco_info, cache_only=True)
app.nec.push_incoming_event(
NetworkEvent('caps-update',
account=self._account,
fjid=jid,
jid=properties.jid.bare))
def _execute_task(self, task):
self._log.info('Request %s from %s', task.entity.hash, task.entity.jid)
self._con.get_module('Discovery').disco_info(
task.entity.jid,
node=f'{task.entity.node}#{task.entity.hash}',
callback=self._on_disco_info,
user_data=task.entity.jid)
def _on_disco_info(self, nbxmpp_task):
jid = nbxmpp_task.get_user_data()
task = self._get_task(jid)
if task is None:
self._log.info('Task not found for %s', jid)
return
self._remove_task(task)
try:
disco_info = nbxmpp_task.finish()
except StanzaError as error:
self._log.warning(error)
return
self._log.info('Disco Info received: %s', disco_info.jid)
try:
compute_caps_hash(disco_info)
except Exception as error:
self._log.warning('Disco info malformed: %s %s',
disco_info.jid, error)
return
app.storage.cache.add_caps_entry(
str(disco_info.jid),
task.entity.method,
disco_info.get_caps_hash(),
disco_info)
self._log.info('Finished query for %s', task.entity.hash)
tasks = self._get_similar_tasks(task)
for task in tasks:
self._remove_task(task)
self._log.info('Update %s', task.entity.jid)
app.nec.push_incoming_event(
NetworkEvent('caps-update',
account=self._account,
fjid=str(task.entity.jid),
jid=task.entity.jid.bare))
def update_caps(self):
if not app.account_is_connected(self._account):
return
optional_features = get_optional_features(self._account)
self.set_caps(self._identities,
COMMON_FEATURES + optional_features,
'https://gajim.org')
if not app.account_is_available(self._account):
return
app.connections[self._account].change_status(
app.connections[self._account].status,
app.connections[self._account].status_message)
def cleanup(self):
self._remove_all_tasks()
BaseModule.cleanup(self)
class EntityCapsTask(Task):
def __init__(self, account, properties, callback):
Task.__init__(self)
self._account = account
self._callback = weakref.WeakMethod(callback)
self.entity = Entity(jid=properties.jid,
node=properties.entity_caps.node,
hash=properties.entity_caps.ver,
method=properties.entity_caps.hash)
self._from_muc = properties.from_muc
def execute(self):
callback = self._callback()
if callback is not None:
callback(self)
def preconditions_met(self):
try:
client = app.get_client(self._account)
except Exception:
return False
if self._from_muc:
muc = client.get_module('MUC').get_manager().get(
self.entity.jid.bare)
if muc is None or not muc.state.is_joined:
self.set_obsolete()
return False
return client.state.is_available
def __repr__(self):
return f'Entity Caps ({self.entity.jid} {self.entity.hash})'
def __hash__(self):
return hash(self.entity)
def get_instance(*args, **kwargs):
return Caps(*args, **kwargs), 'Caps'