9578053 Jan 22 2022 distfiles.gentoo.org/distfiles/gajim-1.3.3-2.tar.gz
This commit is contained in:
parent
a5b3822651
commit
4c1b226bff
1045 changed files with 753037 additions and 18 deletions
174
gajim/common/modules/__init__.py
Normal file
174
gajim/common/modules/__init__.py
Normal file
|
@ -0,0 +1,174 @@
|
|||
# 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/>.
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict # pylint: disable=unused-import
|
||||
from typing import List
|
||||
from typing import Tuple
|
||||
|
||||
import sys
|
||||
import logging
|
||||
from importlib import import_module
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from gajim.common.types import ConnectionT
|
||||
|
||||
log = logging.getLogger('gajim.c.m')
|
||||
|
||||
ZEROCONF_MODULES = ['iq',
|
||||
'adhoc_commands',
|
||||
'receipts',
|
||||
'discovery',
|
||||
'chatstates']
|
||||
|
||||
MODULES = [
|
||||
'adhoc_commands',
|
||||
'annotations',
|
||||
'bits_of_binary',
|
||||
'blocking',
|
||||
'bookmarks',
|
||||
'caps',
|
||||
'carbons',
|
||||
'chat_markers',
|
||||
'chatstates',
|
||||
'delimiter',
|
||||
'discovery',
|
||||
'entity_time',
|
||||
'gateway',
|
||||
'httpupload',
|
||||
'http_auth',
|
||||
'iq',
|
||||
'last_activity',
|
||||
'mam',
|
||||
'message',
|
||||
'metacontacts',
|
||||
'muc',
|
||||
'pep',
|
||||
'ping',
|
||||
'presence',
|
||||
'pubsub',
|
||||
'receipts',
|
||||
'register',
|
||||
'roster',
|
||||
'roster_item_exchange',
|
||||
'search',
|
||||
'security_labels',
|
||||
'software_version',
|
||||
'user_activity',
|
||||
'user_avatar',
|
||||
'user_location',
|
||||
'user_mood',
|
||||
'user_nickname',
|
||||
'user_tune',
|
||||
'vcard4',
|
||||
'vcard_avatars',
|
||||
'vcard_temp',
|
||||
'announce',
|
||||
'ibb',
|
||||
'jingle',
|
||||
'bytestream',
|
||||
]
|
||||
|
||||
_imported_modules = [] # type: List[tuple]
|
||||
_modules = {} # type: Dict[str, Dict[str, Any]]
|
||||
_store_publish_modules = [
|
||||
'UserMood',
|
||||
'UserActivity',
|
||||
'UserLocation',
|
||||
'UserTune',
|
||||
] # type: List[str]
|
||||
|
||||
|
||||
class ModuleMock:
|
||||
def __init__(self, name: str) -> None:
|
||||
self._name = name
|
||||
|
||||
# HTTPUpload, ..
|
||||
self.available = False
|
||||
|
||||
# Blocking
|
||||
self.blocked = [] # type: List[Any]
|
||||
|
||||
# Delimiter
|
||||
self.delimiter = '::'
|
||||
|
||||
# Bookmarks
|
||||
self.bookmarks = {} # type: Dict[Any, Any]
|
||||
|
||||
# Various Modules
|
||||
self.supported = False
|
||||
|
||||
def __getattr__(self, key: str) -> MagicMock:
|
||||
return MagicMock()
|
||||
|
||||
|
||||
def register_modules(con: ConnectionT, *args: Any, **kwargs: Any) -> None:
|
||||
if con in _modules:
|
||||
return
|
||||
_modules[con.name] = {}
|
||||
for module_name in MODULES:
|
||||
if con.name == 'Local':
|
||||
if module_name not in ZEROCONF_MODULES:
|
||||
continue
|
||||
instance, name = _load_module(module_name, con, *args, **kwargs)
|
||||
_modules[con.name][name] = instance
|
||||
|
||||
|
||||
def register_single_module(con: ConnectionT, instance: Any, name: str) -> None:
|
||||
if con.name not in _modules:
|
||||
raise ValueError('Unknown account name: %s' % con.name)
|
||||
_modules[con.name][name] = instance
|
||||
|
||||
|
||||
def unregister_modules(con: ConnectionT) -> None:
|
||||
for instance in _modules[con.name].values():
|
||||
if hasattr(instance, 'cleanup'):
|
||||
instance.cleanup()
|
||||
del _modules[con.name]
|
||||
|
||||
|
||||
def unregister_single_module(con: ConnectionT, name: str) -> None:
|
||||
if con.name not in _modules:
|
||||
return
|
||||
if name not in _modules[con.name]:
|
||||
return
|
||||
del _modules[con.name][name]
|
||||
|
||||
|
||||
def send_stored_publish(account: str) -> None:
|
||||
for name in _store_publish_modules:
|
||||
_modules[account][name].send_stored_publish()
|
||||
|
||||
|
||||
def get(account: str, name: str) -> Any:
|
||||
try:
|
||||
return _modules[account][name]
|
||||
except KeyError:
|
||||
return ModuleMock(name)
|
||||
|
||||
|
||||
def _load_module(name: str, con: ConnectionT, *args: Any, **kwargs: Any) -> Any:
|
||||
if name not in MODULES:
|
||||
raise ValueError('Module %s does not exist' % name)
|
||||
module = sys.modules.get(name)
|
||||
if module is None:
|
||||
module = import_module('.%s' % name, package='gajim.common.modules')
|
||||
return module.get_instance(con, *args, **kwargs) # type: ignore
|
||||
|
||||
|
||||
def get_handlers(con: ConnectionT) -> List[Tuple[Any, ...]]:
|
||||
handlers = [] # type: List[Tuple[Any, ...]]
|
||||
for module in _modules[con.name].values():
|
||||
handlers += module.handlers
|
||||
return handlers
|
434
gajim/common/modules/adhoc_commands.py
Normal file
434
gajim/common/modules/adhoc_commands.py
Normal file
|
@ -0,0 +1,434 @@
|
|||
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
|
||||
# Copyright (C) 2006-2007 Tomasz Melcer <liori AT exroot.org>
|
||||
# Copyright (C) 2007 Jean-Marie Traissard <jim AT lapin.org>
|
||||
# Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
|
||||
# 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/>.
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
from nbxmpp.modules import dataforms
|
||||
from nbxmpp.util import generate_id
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common import helpers
|
||||
from gajim.common.i18n import _
|
||||
from gajim.common.nec import NetworkIncomingEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class AdHocCommand:
|
||||
commandnode = 'command'
|
||||
commandname = 'The Command'
|
||||
commandfeatures = (Namespace.DATA,)
|
||||
|
||||
@staticmethod
|
||||
def is_visible_for(_samejid):
|
||||
"""
|
||||
This returns True if that command should be visible and invocable for
|
||||
others
|
||||
|
||||
samejid - True when command is invoked by an entity with the same bare
|
||||
jid.
|
||||
"""
|
||||
return True
|
||||
|
||||
def __init__(self, conn, jid, sessionid):
|
||||
self.connection = conn
|
||||
self.jid = jid
|
||||
self.sessionid = sessionid
|
||||
|
||||
def build_response(self, request, status='executing', defaultaction=None,
|
||||
actions=None):
|
||||
assert status in ('executing', 'completed', 'canceled')
|
||||
|
||||
response = request.buildReply('result')
|
||||
cmd = response.getTag('command', namespace=Namespace.COMMANDS)
|
||||
cmd.setAttr('sessionid', self.sessionid)
|
||||
cmd.setAttr('node', self.commandnode)
|
||||
cmd.setAttr('status', status)
|
||||
if defaultaction is not None or actions is not None:
|
||||
if defaultaction is not None:
|
||||
assert defaultaction in ('cancel', 'execute', 'prev', 'next',
|
||||
'complete')
|
||||
attrs = {'action': defaultaction}
|
||||
else:
|
||||
attrs = {}
|
||||
|
||||
cmd.addChild('actions', attrs, actions)
|
||||
return response, cmd
|
||||
|
||||
def bad_request(self, stanza):
|
||||
self.connection.connection.send(
|
||||
nbxmpp.Error(stanza, Namespace.STANZAS + ' bad-request'))
|
||||
|
||||
def cancel(self, request):
|
||||
response = self.build_response(request, status='canceled')[0]
|
||||
self.connection.connection.send(response)
|
||||
return False # finish the session
|
||||
|
||||
|
||||
class ChangeStatusCommand(AdHocCommand):
|
||||
commandnode = 'change-status'
|
||||
commandname = _('Change status information')
|
||||
|
||||
def __init__(self, conn, jid, sessionid):
|
||||
AdHocCommand.__init__(self, conn, jid, sessionid)
|
||||
self._callback = self.first_step
|
||||
|
||||
@staticmethod
|
||||
def is_visible_for(samejid):
|
||||
"""
|
||||
Change status is visible only if the entity has the same bare jid
|
||||
"""
|
||||
return samejid
|
||||
|
||||
def execute(self, request):
|
||||
return self._callback(request)
|
||||
|
||||
def first_step(self, request):
|
||||
# first query...
|
||||
response, cmd = self.build_response(request,
|
||||
defaultaction='execute',
|
||||
actions=['execute'])
|
||||
|
||||
cmd.addChild(
|
||||
node=dataforms.SimpleDataForm(
|
||||
title=_('Change status'),
|
||||
instructions=_('Set the presence type and description'),
|
||||
fields=[
|
||||
dataforms.create_field(
|
||||
'list-single',
|
||||
var='presence-type',
|
||||
label='Type of presence:',
|
||||
options=[
|
||||
('chat', _('Free for chat')),
|
||||
('online', _('Online')),
|
||||
('away', _('Away')),
|
||||
('xa', _('Extended away')),
|
||||
('dnd', _('Do not disturb')),
|
||||
('offline', _('Offline - disconnect'))],
|
||||
value='online',
|
||||
required=True),
|
||||
dataforms.create_field(
|
||||
'text-multi',
|
||||
var='presence-desc',
|
||||
label=_('Presence description:'))
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
self.connection.connection.send(response)
|
||||
|
||||
# for next invocation
|
||||
self._callback = self.second_step
|
||||
|
||||
return True # keep the session
|
||||
|
||||
def second_step(self, request):
|
||||
# check if the data is correct
|
||||
try:
|
||||
form = dataforms.SimpleDataForm(
|
||||
extend=request.getTag('command').getTag('x'))
|
||||
except Exception:
|
||||
self.bad_request(request)
|
||||
return False
|
||||
|
||||
try:
|
||||
presencetype = form['presence-type'].value
|
||||
if presencetype not in ('chat', 'online', 'away',
|
||||
'xa', 'dnd', 'offline'):
|
||||
self.bad_request(request)
|
||||
return False
|
||||
except Exception:
|
||||
# KeyError if there's no presence-type field in form or
|
||||
# AttributeError if that field is of wrong type
|
||||
self.bad_request(request)
|
||||
return False
|
||||
|
||||
try:
|
||||
presencedesc = form['presence-desc'].value
|
||||
except Exception: # same exceptions as in last comment
|
||||
presencedesc = ''
|
||||
|
||||
response, cmd = self.build_response(request, status='completed')
|
||||
cmd.addChild('note', {}, _('The status has been changed.'))
|
||||
|
||||
# if going offline, we need to push response so it won't go into
|
||||
# queue and disappear
|
||||
self.connection.connection.send(response,
|
||||
now=presencetype == 'offline')
|
||||
|
||||
# send new status
|
||||
app.interface.roster.send_status(
|
||||
self.connection.name, presencetype, presencedesc)
|
||||
|
||||
return False # finish the session
|
||||
|
||||
|
||||
class AdHocCommands(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'AdHoc'
|
||||
_nbxmpp_methods = [
|
||||
'request_command_list',
|
||||
'execute_command',
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='iq',
|
||||
callback=self._execute_command_received,
|
||||
typ='set',
|
||||
ns=Namespace.COMMANDS),
|
||||
]
|
||||
|
||||
# a list of all commands exposed: node -> command class
|
||||
self._commands = {}
|
||||
if app.settings.get('remote_commands'):
|
||||
for cmdobj in (ChangeStatusCommand,):
|
||||
self._commands[cmdobj.commandnode] = cmdobj
|
||||
|
||||
# a list of sessions; keys are tuples (jid, sessionid, node)
|
||||
self._sessions = {}
|
||||
|
||||
def get_own_bare_jid(self):
|
||||
return self._con.get_own_jid().bare
|
||||
|
||||
def is_same_jid(self, jid):
|
||||
"""
|
||||
Test if the bare jid given is the same as our bare jid
|
||||
"""
|
||||
return nbxmpp.JID.from_string(jid).bare == self.get_own_bare_jid()
|
||||
|
||||
def command_list_query(self, stanza):
|
||||
iq = stanza.buildReply('result')
|
||||
jid = helpers.get_full_jid_from_iq(stanza)
|
||||
query = iq.getTag('query')
|
||||
# buildReply don't copy the node attribute. Re-add it
|
||||
query.setAttr('node', Namespace.COMMANDS)
|
||||
|
||||
for node, cmd in self._commands.items():
|
||||
if cmd.is_visible_for(self.is_same_jid(jid)):
|
||||
query.addChild('item', {
|
||||
# TODO: find the jid
|
||||
'jid': str(self._con.get_own_jid()),
|
||||
'node': node,
|
||||
'name': cmd.commandname})
|
||||
|
||||
self._con.connection.send(iq)
|
||||
|
||||
def command_info_query(self, stanza):
|
||||
"""
|
||||
Send disco#info result for query for command (XEP-0050, example 6.).
|
||||
Return True if the result was sent, False if not
|
||||
"""
|
||||
try:
|
||||
jid = helpers.get_full_jid_from_iq(stanza)
|
||||
except helpers.InvalidFormat:
|
||||
self._log.warning('Invalid JID: %s, ignoring it', stanza.getFrom())
|
||||
return False
|
||||
|
||||
node = stanza.getTagAttr('query', 'node')
|
||||
|
||||
if node not in self._commands:
|
||||
return False
|
||||
|
||||
cmd = self._commands[node]
|
||||
if cmd.is_visible_for(self.is_same_jid(jid)):
|
||||
iq = stanza.buildReply('result')
|
||||
query = iq.getTag('query')
|
||||
query.addChild('identity',
|
||||
attrs={'type': 'command-node',
|
||||
'category': 'automation',
|
||||
'name': cmd.commandname})
|
||||
query.addChild('feature', attrs={'var': Namespace.COMMANDS})
|
||||
for feature in cmd.commandfeatures:
|
||||
query.addChild('feature', attrs={'var': feature})
|
||||
|
||||
self._con.connection.send(iq)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def command_items_query(self, stanza):
|
||||
"""
|
||||
Send disco#items result for query for command.
|
||||
Return True if the result was sent, False if not.
|
||||
"""
|
||||
jid = helpers.get_full_jid_from_iq(stanza)
|
||||
node = stanza.getTagAttr('query', 'node')
|
||||
|
||||
if node not in self._commands:
|
||||
return False
|
||||
|
||||
cmd = self._commands[node]
|
||||
if cmd.is_visible_for(self.is_same_jid(jid)):
|
||||
iq = stanza.buildReply('result')
|
||||
self._con.connection.send(iq)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _execute_command_received(self, _con, stanza, _properties):
|
||||
jid = helpers.get_full_jid_from_iq(stanza)
|
||||
|
||||
cmd = stanza.getTag('command')
|
||||
if cmd is None:
|
||||
self._log.error('Malformed stanza (no command node) %s', stanza)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
node = cmd.getAttr('node')
|
||||
if node is None:
|
||||
self._log.error('Malformed stanza (no node attr) %s', stanza)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
sessionid = cmd.getAttr('sessionid')
|
||||
if sessionid is None:
|
||||
# we start a new command session
|
||||
# only if we are visible for the jid and command exist
|
||||
if node not in self._commands.keys():
|
||||
self._con.connection.send(
|
||||
nbxmpp.Error(
|
||||
stanza, Namespace.STANZAS + ' item-not-found'))
|
||||
self._log.warning('Comand %s does not exist: %s', node, jid)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
newcmd = self._commands[node]
|
||||
if not newcmd.is_visible_for(self.is_same_jid(jid)):
|
||||
self._log.warning('Command not visible for jid: %s', jid)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
# generate new sessionid
|
||||
sessionid = generate_id()
|
||||
|
||||
# create new instance and run it
|
||||
obj = newcmd(conn=self, jid=jid, sessionid=sessionid)
|
||||
rc = obj.execute(stanza)
|
||||
if rc:
|
||||
self._sessions[(jid, sessionid, node)] = obj
|
||||
self._log.info('Comand %s executed: %s', node, jid)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
# the command is already running, check for it
|
||||
magictuple = (jid, sessionid, node)
|
||||
if magictuple not in self._sessions:
|
||||
# we don't have this session... ha!
|
||||
self._log.warning('Invalid session %s', magictuple)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
action = cmd.getAttr('action')
|
||||
obj = self._sessions[magictuple]
|
||||
|
||||
try:
|
||||
if action == 'cancel':
|
||||
rc = obj.cancel(stanza)
|
||||
elif action == 'prev':
|
||||
rc = obj.prev(stanza)
|
||||
elif action == 'next':
|
||||
rc = obj.next(stanza)
|
||||
elif action == 'execute' or action is None:
|
||||
rc = obj.execute(stanza)
|
||||
elif action == 'complete':
|
||||
rc = obj.complete(stanza)
|
||||
else:
|
||||
# action is wrong. stop the session, send error
|
||||
raise AttributeError
|
||||
except AttributeError:
|
||||
# the command probably doesn't handle invoked action...
|
||||
# stop the session, return error
|
||||
del self._sessions[magictuple]
|
||||
self._log.warning('Wrong action %s %s', node, jid)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
# delete the session if rc is False
|
||||
if not rc:
|
||||
del self._sessions[magictuple]
|
||||
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def send_command(self, jid, node, session_id,
|
||||
form, action='execute'):
|
||||
"""
|
||||
Send the command with data form. Wait for reply
|
||||
"""
|
||||
self._log.info('Send Command: %s %s %s %s',
|
||||
jid, node, session_id, action)
|
||||
stanza = nbxmpp.Iq(typ='set', to=jid)
|
||||
cmdnode = stanza.addChild('command',
|
||||
namespace=Namespace.COMMANDS,
|
||||
attrs={'node': node,
|
||||
'action': action})
|
||||
|
||||
if session_id:
|
||||
cmdnode.setAttr('sessionid', session_id)
|
||||
|
||||
if form:
|
||||
cmdnode.addChild(node=form.get_purged())
|
||||
|
||||
self._con.connection.SendAndCallForResponse(
|
||||
stanza, self._action_response_received)
|
||||
|
||||
def _action_response_received(self, _nbxmpp_client, stanza):
|
||||
if not nbxmpp.isResultNode(stanza):
|
||||
self._log.info('Error: %s', stanza.getError())
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
AdHocCommandError(None, conn=self._con,
|
||||
error=stanza.getError()))
|
||||
return
|
||||
self._log.info('Received action response')
|
||||
command = stanza.getTag('command')
|
||||
app.nec.push_incoming_event(
|
||||
AdHocCommandActionResponse(
|
||||
None, conn=self._con, command=command))
|
||||
|
||||
def send_cancel(self, jid, node, session_id):
|
||||
"""
|
||||
Send the command with action='cancel'
|
||||
"""
|
||||
self._log.info('Cancel: %s %s %s', jid, node, session_id)
|
||||
stanza = nbxmpp.Iq(typ='set', to=jid)
|
||||
stanza.addChild('command', namespace=Namespace.COMMANDS,
|
||||
attrs={
|
||||
'node': node,
|
||||
'sessionid': session_id,
|
||||
'action': 'cancel'
|
||||
})
|
||||
|
||||
self._con.connection.SendAndCallForResponse(
|
||||
stanza, self._cancel_result_received)
|
||||
|
||||
def _cancel_result_received(self, _nbxmpp_client, stanza):
|
||||
if not nbxmpp.isResultNode(stanza):
|
||||
self._log.warning('Error: %s', stanza.getError())
|
||||
else:
|
||||
self._log.info('Cancel successful')
|
||||
|
||||
|
||||
class AdHocCommandError(NetworkIncomingEvent):
|
||||
name = 'adhoc-command-error'
|
||||
|
||||
|
||||
class AdHocCommandActionResponse(NetworkIncomingEvent):
|
||||
name = 'adhoc-command-action-response'
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return AdHocCommands(*args, **kwargs), 'AdHocCommands'
|
66
gajim/common/modules/annotations.py
Normal file
66
gajim/common/modules/annotations.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
# 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-0145: Annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict # pylint: disable=unused-import
|
||||
from typing import Tuple
|
||||
|
||||
from nbxmpp.errors import StanzaError
|
||||
from nbxmpp.errors import MalformedStanzaError
|
||||
from nbxmpp.structs import AnnotationNote
|
||||
|
||||
from gajim.common.types import ConnectionT
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class Annotations(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'Annotations'
|
||||
_nbxmpp_methods = [
|
||||
'request_annotations',
|
||||
'set_annotations',
|
||||
]
|
||||
|
||||
def __init__(self, con: ConnectionT) -> None:
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self._annotations = {} # type: Dict[str, AnnotationNote]
|
||||
|
||||
def request_annotations(self) -> None:
|
||||
self._nbxmpp('Annotations').request_annotations(
|
||||
callback=self._annotations_received)
|
||||
|
||||
def _annotations_received(self, task: Any) -> None:
|
||||
try:
|
||||
annotations = task.finish()
|
||||
except (StanzaError, MalformedStanzaError) as error:
|
||||
self._log.warning(error)
|
||||
self._annotations = {}
|
||||
return
|
||||
|
||||
for note in annotations:
|
||||
self._annotations[note.jid] = note
|
||||
|
||||
def get_note(self, jid: str) -> AnnotationNote:
|
||||
return self._annotations.get(jid)
|
||||
|
||||
def set_note(self, note: AnnotationNote) -> None:
|
||||
self._annotations[note.jid] = note
|
||||
self.set_annotations(self._annotations.values())
|
||||
|
||||
|
||||
def get_instance(*args: Any, **kwargs: Any) -> Tuple[Annotations, str]:
|
||||
return Annotations(*args, **kwargs), 'Annotations'
|
37
gajim/common/modules/announce.py
Normal file
37
gajim/common/modules/announce.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
# 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/>.
|
||||
|
||||
# Server MOTD and Announce
|
||||
|
||||
import nbxmpp
|
||||
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class Announce(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
def delete_motd(self):
|
||||
server = self._con.get_own_jid().domain
|
||||
jid = '%s/announce/motd/delete' % server
|
||||
self.set_announce(jid)
|
||||
|
||||
def set_announce(self, jid, subject=None, body=None):
|
||||
message = nbxmpp.Message(to=jid, body=body, subject=subject)
|
||||
self._nbxmpp().send(message)
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return Announce(*args, **kwargs), 'Announce'
|
97
gajim/common/modules/base.py
Normal file
97
gajim/common/modules/base.py
Normal file
|
@ -0,0 +1,97 @@
|
|||
# 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/>.
|
||||
|
||||
from typing import Any # pylint: disable=unused-import
|
||||
from typing import Dict # pylint: disable=unused-import
|
||||
from typing import List # pylint: disable=unused-import
|
||||
|
||||
import logging
|
||||
from functools import partial
|
||||
from unittest.mock import Mock
|
||||
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import EventHelper
|
||||
from gajim.common.modules.util import LogAdapter
|
||||
|
||||
|
||||
class BaseModule(EventHelper):
|
||||
|
||||
_nbxmpp_extends = ''
|
||||
_nbxmpp_methods = [] # type: List[str]
|
||||
|
||||
def __init__(self, con, *args, plugin=False, **kwargs):
|
||||
EventHelper.__init__(self)
|
||||
self._con = con
|
||||
self._account = con.name
|
||||
self._log = self._set_logger(plugin)
|
||||
self._nbxmpp_callbacks = {} # type: Dict[str, Any]
|
||||
self._stored_publish = None # type: Callable
|
||||
self.handlers = [] # type: List[str]
|
||||
|
||||
def _set_logger(self, plugin):
|
||||
logger_name = 'gajim.c.m.%s'
|
||||
if plugin:
|
||||
logger_name = 'gajim.p.%s'
|
||||
logger_name = logger_name % self.__class__.__name__.lower()
|
||||
logger = logging.getLogger(logger_name)
|
||||
return LogAdapter(logger, {'account': self._account})
|
||||
|
||||
def __getattr__(self, key):
|
||||
if key not in self._nbxmpp_methods:
|
||||
raise AttributeError(
|
||||
"attribute '%s' is neither part of object '%s' "
|
||||
" nor declared in '_nbxmpp_methods'" % (
|
||||
key, self.__class__.__name__))
|
||||
|
||||
if not app.account_is_connected(self._account):
|
||||
self._log.warning('Account not connected, can’t use %s', key)
|
||||
return None
|
||||
|
||||
module = self._con.connection.get_module(self._nbxmpp_extends)
|
||||
|
||||
callback = self._nbxmpp_callbacks.get(key)
|
||||
if callback is None:
|
||||
return getattr(module, key)
|
||||
return partial(getattr(module, key), callback=callback)
|
||||
|
||||
def _nbxmpp(self, module_name=None):
|
||||
if not app.account_is_connected(self._account):
|
||||
self._log.warning('Account not connected, can’t use nbxmpp method')
|
||||
return Mock()
|
||||
|
||||
if module_name is None:
|
||||
return self._con.connection
|
||||
return self._con.connection.get_module(module_name)
|
||||
|
||||
def _register_callback(self, method, callback):
|
||||
self._nbxmpp_callbacks[method] = callback
|
||||
|
||||
def _register_pubsub_handler(self, callback):
|
||||
handler = StanzaHandler(name='message',
|
||||
callback=callback,
|
||||
ns=Namespace.PUBSUB_EVENT,
|
||||
priority=49)
|
||||
self.handlers.append(handler)
|
||||
|
||||
def send_stored_publish(self):
|
||||
if self._stored_publish is None:
|
||||
return
|
||||
self._log.info('Send stored publish')
|
||||
self._stored_publish() # pylint: disable=not-callable
|
||||
|
||||
def cleanup(self):
|
||||
self.unregister_events()
|
204
gajim/common/modules/bits_of_binary.py
Normal file
204
gajim/common/modules/bits_of_binary.py
Normal file
|
@ -0,0 +1,204 @@
|
|||
# Copyright (C) 2018 Emmanuel Gil Peyrot <linkmauve AT linkmauve.fr>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
import logging
|
||||
import hashlib
|
||||
from base64 import b64decode
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common import configpaths
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
log = logging.getLogger('gajim.c.m.bob')
|
||||
|
||||
|
||||
class BitsOfBinary(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='iq',
|
||||
callback=self._answer_bob_request,
|
||||
typ='get',
|
||||
ns=Namespace.BOB),
|
||||
]
|
||||
|
||||
# Used to track which cids are in-flight.
|
||||
self.awaiting_cids = {}
|
||||
|
||||
def _answer_bob_request(self, _con, stanza, _properties):
|
||||
self._log.info('Request from %s for BoB data', stanza.getFrom())
|
||||
iq = stanza.buildReply('error')
|
||||
err = nbxmpp.ErrorNode(nbxmpp.ERR_ITEM_NOT_FOUND)
|
||||
iq.addChild(node=err)
|
||||
self._log.info('Sending item-not-found')
|
||||
self._con.connection.send(iq)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _on_bob_received(self, _nbxmpp_client, result, cid):
|
||||
"""
|
||||
Called when we receive BoB data
|
||||
"""
|
||||
if cid not in self.awaiting_cids:
|
||||
return
|
||||
|
||||
if result.getType() == 'result':
|
||||
data = result.getTags('data', namespace=Namespace.BOB)
|
||||
if data.getAttr('cid') == cid:
|
||||
for func in self.awaiting_cids[cid]:
|
||||
cb = func[0]
|
||||
args = func[1]
|
||||
pos = func[2]
|
||||
bob_data = data.getData()
|
||||
def recurs(node, cid, data):
|
||||
if node.getData() == 'cid:' + cid:
|
||||
node.setData(data)
|
||||
else:
|
||||
for child in node.getChildren():
|
||||
recurs(child, cid, data)
|
||||
recurs(args[pos], cid, bob_data)
|
||||
cb(*args)
|
||||
del self.awaiting_cids[cid]
|
||||
return
|
||||
|
||||
# An error occurred, call callback without modifying data.
|
||||
for func in self.awaiting_cids[cid]:
|
||||
cb = func[0]
|
||||
args = func[1]
|
||||
cb(*args)
|
||||
del self.awaiting_cids[cid]
|
||||
|
||||
def get_bob_data(self, cid, to, callback, args, position):
|
||||
"""
|
||||
Request for BoB (XEP-0231) and when data will arrive, call callback
|
||||
with given args, after having replaced cid by it's data in
|
||||
args[position]
|
||||
"""
|
||||
if cid in self.awaiting_cids:
|
||||
self.awaiting_cids[cid].appends((callback, args, position))
|
||||
else:
|
||||
self.awaiting_cids[cid] = [(callback, args, position)]
|
||||
iq = nbxmpp.Iq(to=to, typ='get')
|
||||
iq.addChild(name='data', attrs={'cid': cid}, namespace=Namespace.BOB)
|
||||
self._con.connection.SendAndCallForResponse(
|
||||
iq, self._on_bob_received, {'cid': cid})
|
||||
|
||||
|
||||
def parse_bob_data(stanza):
|
||||
data_node = stanza.getTag('data', namespace=Namespace.BOB)
|
||||
if data_node is None:
|
||||
return None
|
||||
|
||||
cid = data_node.getAttr('cid')
|
||||
type_ = data_node.getAttr('type')
|
||||
max_age = data_node.getAttr('max-age')
|
||||
if max_age is not None:
|
||||
try:
|
||||
max_age = int(max_age)
|
||||
except Exception:
|
||||
log.exception(stanza)
|
||||
return None
|
||||
|
||||
if cid is None or type_ is None:
|
||||
log.warning('Invalid data node (no cid or type attr): %s', stanza)
|
||||
return None
|
||||
|
||||
try:
|
||||
algo_hash = cid.split('@')[0]
|
||||
algo, hash_ = algo_hash.split('+')
|
||||
except Exception:
|
||||
log.exception('Invalid cid: %s', stanza)
|
||||
return None
|
||||
|
||||
bob_data = data_node.getData()
|
||||
if not bob_data:
|
||||
log.warning('No data found: %s', stanza)
|
||||
return None
|
||||
|
||||
filepath = configpaths.get('BOB') / algo_hash
|
||||
if algo_hash in app.bob_cache or filepath.exists():
|
||||
log.info('BoB data already cached')
|
||||
return None
|
||||
|
||||
try:
|
||||
bob_data = b64decode(bob_data)
|
||||
except Exception:
|
||||
log.warning('Unable to decode data')
|
||||
log.exception(stanza)
|
||||
return None
|
||||
|
||||
if len(bob_data) > 10000:
|
||||
log.warning('%s: data > 10000 bytes', stanza.getFrom())
|
||||
return None
|
||||
|
||||
try:
|
||||
sha = hashlib.new(algo)
|
||||
except ValueError as error:
|
||||
log.warning(stanza)
|
||||
log.warning(error)
|
||||
return None
|
||||
|
||||
sha.update(bob_data)
|
||||
if sha.hexdigest() != hash_:
|
||||
log.warning('Invalid hash: %s', stanza)
|
||||
return None
|
||||
|
||||
if max_age == 0:
|
||||
app.bob_cache[algo_hash] = bob_data
|
||||
else:
|
||||
try:
|
||||
with open(str(filepath), 'w+b') as file:
|
||||
file.write(bob_data)
|
||||
except Exception:
|
||||
log.warning('Unable to save data')
|
||||
log.exception(stanza)
|
||||
return None
|
||||
|
||||
log.info('BoB data stored: %s', algo_hash)
|
||||
return filepath
|
||||
|
||||
|
||||
def store_bob_data(bob_data):
|
||||
if bob_data is None:
|
||||
return None
|
||||
|
||||
algo_hash = '%s+%s' % (bob_data.algo, bob_data.hash_)
|
||||
|
||||
filepath = configpaths.get('BOB') / algo_hash
|
||||
if algo_hash in app.bob_cache or filepath.exists():
|
||||
log.info('BoB data already cached')
|
||||
return None
|
||||
|
||||
if bob_data.max_age == 0:
|
||||
app.bob_cache[algo_hash] = bob_data.data
|
||||
else:
|
||||
try:
|
||||
with open(str(filepath), 'w+b') as file:
|
||||
file.write(bob_data.data)
|
||||
except Exception:
|
||||
log.exception('Unable to save data')
|
||||
return None
|
||||
|
||||
log.info('BoB data stored: %s', algo_hash)
|
||||
return filepath
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return BitsOfBinary(*args, **kwargs), 'BitsOfBinary'
|
139
gajim/common/modules/blocking.py
Normal file
139
gajim/common/modules/blocking.py
Normal file
|
@ -0,0 +1,139 @@
|
|||
# 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-0191: Blocking Command
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.protocol import JID
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
from nbxmpp.modules.util import raise_if_error
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.modules.util import as_task
|
||||
|
||||
|
||||
class Blocking(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'Blocking'
|
||||
_nbxmpp_methods = [
|
||||
'block',
|
||||
'unblock',
|
||||
'request_blocking_list',
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.blocked = []
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='iq',
|
||||
callback=self._blocking_push_received,
|
||||
typ='set',
|
||||
ns=Namespace.BLOCKING),
|
||||
]
|
||||
|
||||
self.supported = False
|
||||
|
||||
def pass_disco(self, info):
|
||||
if Namespace.BLOCKING not in info.features:
|
||||
return
|
||||
|
||||
self.supported = True
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('feature-discovered',
|
||||
account=self._account,
|
||||
feature=Namespace.BLOCKING))
|
||||
|
||||
self._log.info('Discovered blocking: %s', info.jid)
|
||||
|
||||
@as_task
|
||||
def get_blocking_list(self):
|
||||
_task = yield
|
||||
|
||||
blocking_list = yield self._nbxmpp('Blocking').request_blocking_list()
|
||||
|
||||
raise_if_error(blocking_list)
|
||||
|
||||
self.blocked = list(blocking_list)
|
||||
app.nec.push_incoming_event(NetworkEvent('blocking',
|
||||
conn=self._con,
|
||||
changed=self.blocked))
|
||||
yield blocking_list
|
||||
|
||||
@as_task
|
||||
def update_blocking_list(self, block, unblock):
|
||||
_task = yield
|
||||
|
||||
if block:
|
||||
result = yield self.block(block)
|
||||
raise_if_error(result)
|
||||
|
||||
if unblock:
|
||||
result = yield self.unblock(unblock)
|
||||
raise_if_error(result)
|
||||
|
||||
yield True
|
||||
|
||||
def _blocking_push_received(self, _con, _stanza, properties):
|
||||
if not properties.is_blocking:
|
||||
return
|
||||
|
||||
changed_list = []
|
||||
|
||||
if properties.blocking.unblock_all:
|
||||
self.blocked = []
|
||||
for jid in self.blocked:
|
||||
self._presence_probe(jid)
|
||||
self._log.info('Unblock all Push')
|
||||
|
||||
for jid in properties.blocking.unblock:
|
||||
changed_list.append(jid)
|
||||
if jid not in self.blocked:
|
||||
continue
|
||||
self.blocked.remove(jid)
|
||||
self._presence_probe(jid)
|
||||
self._log.info('Unblock Push: %s', jid)
|
||||
|
||||
for jid in properties.blocking.block:
|
||||
if jid in self.blocked:
|
||||
continue
|
||||
changed_list.append(jid)
|
||||
self.blocked.append(jid)
|
||||
self._set_contact_offline(str(jid))
|
||||
self._log.info('Block Push: %s', jid)
|
||||
|
||||
app.nec.push_incoming_event(NetworkEvent('blocking',
|
||||
conn=self._con,
|
||||
changed=changed_list))
|
||||
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _set_contact_offline(self, jid: str) -> None:
|
||||
contact_list = app.contacts.get_contacts(self._account, jid)
|
||||
for contact in contact_list:
|
||||
contact.show = 'offline'
|
||||
|
||||
def _presence_probe(self, jid: JID) -> None:
|
||||
self._log.info('Presence probe: %s', jid)
|
||||
# Send a presence Probe to get the current Status
|
||||
probe = nbxmpp.Presence(jid, 'probe', frm=self._con.get_own_jid())
|
||||
self._nbxmpp().send(probe)
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return Blocking(*args, **kwargs), 'Blocking'
|
353
gajim/common/modules/bookmarks.py
Normal file
353
gajim/common/modules/bookmarks.py
Normal file
|
@ -0,0 +1,353 @@
|
|||
# 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-0048: Bookmarks
|
||||
|
||||
from typing import Any
|
||||
from typing import List
|
||||
from typing import Dict
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
from typing import Optional
|
||||
|
||||
import functools
|
||||
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.protocol import JID
|
||||
from nbxmpp.structs import BookmarkData
|
||||
from gi.repository import GLib
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.modules.util import event_node
|
||||
|
||||
|
||||
NODE_MAX_NS = 'http://jabber.org/protocol/pubsub#config-node-max'
|
||||
|
||||
|
||||
class Bookmarks(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
self._register_pubsub_handler(self._bookmark_event_received)
|
||||
self._register_pubsub_handler(self._bookmark_1_event_received)
|
||||
self._conversion = False
|
||||
self._compat = False
|
||||
self._compat_pep = False
|
||||
self._node_max = False
|
||||
self._bookmarks = {}
|
||||
self._join_timeouts = []
|
||||
self._request_in_progress = True
|
||||
|
||||
@property
|
||||
def conversion(self) -> bool:
|
||||
return self._conversion
|
||||
|
||||
@property
|
||||
def compat(self) -> bool:
|
||||
return self._compat
|
||||
|
||||
@property
|
||||
def compat_pep(self) -> bool:
|
||||
return self._compat_pep
|
||||
|
||||
@property
|
||||
def bookmarks(self) -> List[BookmarkData]:
|
||||
return self._bookmarks.values()
|
||||
|
||||
@property
|
||||
def pep_bookmarks_used(self) -> bool:
|
||||
return self._bookmark_module() == 'PEPBookmarks'
|
||||
|
||||
@property
|
||||
def nativ_bookmarks_used(self) -> bool:
|
||||
return self._bookmark_module() == 'NativeBookmarks'
|
||||
|
||||
@event_node(Namespace.BOOKMARKS)
|
||||
def _bookmark_event_received(self, _con, _stanza, properties):
|
||||
if properties.pubsub_event.retracted:
|
||||
return
|
||||
|
||||
if not properties.is_self_message:
|
||||
self._log.warning('%s has an open access bookmarks node',
|
||||
properties.jid)
|
||||
return
|
||||
|
||||
if not self.pep_bookmarks_used:
|
||||
return
|
||||
|
||||
if self._request_in_progress:
|
||||
self._log.info('Ignore update, pubsub request in progress')
|
||||
return
|
||||
|
||||
bookmarks = self._convert_to_dict(properties.pubsub_event.data)
|
||||
|
||||
old_bookmarks = self._bookmarks.copy()
|
||||
self._bookmarks = bookmarks
|
||||
self._act_on_changed_bookmarks(old_bookmarks)
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('bookmarks-received', account=self._account))
|
||||
|
||||
@event_node(Namespace.BOOKMARKS_1)
|
||||
def _bookmark_1_event_received(self, _con, _stanza, properties):
|
||||
if not properties.is_self_message:
|
||||
self._log.warning('%s has an open access bookmarks node',
|
||||
properties.jid)
|
||||
return
|
||||
|
||||
if not self.nativ_bookmarks_used:
|
||||
return
|
||||
|
||||
if self._request_in_progress:
|
||||
self._log.info('Ignore update, pubsub request in progress')
|
||||
return
|
||||
|
||||
old_bookmarks = self._bookmarks.copy()
|
||||
|
||||
if properties.pubsub_event.deleted or properties.pubsub_event.purged:
|
||||
self._log.info('Bookmark node deleted/purged')
|
||||
self._bookmarks = {}
|
||||
|
||||
elif properties.pubsub_event.retracted:
|
||||
jid = properties.pubsub_event.id
|
||||
self._log.info('Retract: %s', jid)
|
||||
bookmark = self._bookmarks.get(jid)
|
||||
if bookmark is not None:
|
||||
self._bookmarks.pop(bookmark, None)
|
||||
|
||||
else:
|
||||
new_bookmark = properties.pubsub_event.data
|
||||
self._bookmarks[new_bookmark.jid] = properties.pubsub_event.data
|
||||
|
||||
self._act_on_changed_bookmarks(old_bookmarks)
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('bookmarks-received', account=self._account))
|
||||
|
||||
def pass_disco(self, info):
|
||||
self._node_max = NODE_MAX_NS in info.features
|
||||
self._compat_pep = Namespace.BOOKMARKS_COMPAT_PEP in info.features
|
||||
self._compat = Namespace.BOOKMARKS_COMPAT in info.features
|
||||
self._conversion = Namespace.BOOKMARK_CONVERSION in info.features
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _bookmark_module(self):
|
||||
if not self._con.get_module('PubSub').publish_options:
|
||||
return 'PrivateBookmarks'
|
||||
|
||||
if app.settings.get('dev_force_bookmark_2'):
|
||||
return 'NativeBookmarks'
|
||||
|
||||
if self._compat_pep and self._node_max:
|
||||
return 'NativeBookmarks'
|
||||
|
||||
if self._conversion:
|
||||
return 'PEPBookmarks'
|
||||
return 'PrivateBookmarks'
|
||||
|
||||
def _act_on_changed_bookmarks(
|
||||
self, old_bookmarks: Dict[str, BookmarkData]) -> None:
|
||||
|
||||
new_bookmarks = self._convert_to_set(self._bookmarks)
|
||||
old_bookmarks = self._convert_to_set(old_bookmarks)
|
||||
changed = new_bookmarks - old_bookmarks
|
||||
if not changed:
|
||||
return
|
||||
|
||||
join = [jid for jid, autojoin in changed if autojoin]
|
||||
bookmarks = []
|
||||
for jid in join:
|
||||
self._log.info('Schedule autojoin in 10s for: %s', jid)
|
||||
bookmarks.append(self._bookmarks.get(jid))
|
||||
# If another client creates a MUC, the MUC is locked until the
|
||||
# configuration is finished. Give the user some time to finish
|
||||
# the configuration.
|
||||
timeout_id = GLib.timeout_add_seconds(
|
||||
10, self._join_with_timeout, bookmarks)
|
||||
self._join_timeouts.append(timeout_id)
|
||||
|
||||
# TODO: leave mucs
|
||||
# leave = [jid for jid, autojoin in changed if not autojoin]
|
||||
|
||||
@staticmethod
|
||||
def _convert_to_set(
|
||||
bookmarks: Dict[str, BookmarkData]) -> Set[Tuple[str, bool]]:
|
||||
|
||||
set_ = set()
|
||||
for jid, bookmark in bookmarks.items():
|
||||
set_.add((jid, bookmark.autojoin))
|
||||
return set_
|
||||
|
||||
@staticmethod
|
||||
def _convert_to_dict(bookmarks: List) -> Dict[str, BookmarkData]:
|
||||
_dict = {} # type: Dict[str, BookmarkData]
|
||||
if bookmarks is None:
|
||||
return _dict
|
||||
|
||||
for bookmark in bookmarks:
|
||||
_dict[bookmark.jid] = bookmark
|
||||
return _dict
|
||||
|
||||
def get_bookmark(self, jid: Union[str, JID]) -> BookmarkData:
|
||||
return self._bookmarks.get(jid)
|
||||
|
||||
def request_bookmarks(self) -> None:
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
|
||||
self._request_in_progress = True
|
||||
self._nbxmpp(self._bookmark_module()).request_bookmarks(
|
||||
callback=self._bookmarks_received)
|
||||
|
||||
def _bookmarks_received(self, task: Any) -> None:
|
||||
try:
|
||||
bookmarks = task.finish()
|
||||
except Exception as error:
|
||||
self._log.warning(error)
|
||||
bookmarks = None
|
||||
|
||||
self._request_in_progress = False
|
||||
self._bookmarks = self._convert_to_dict(bookmarks)
|
||||
self.auto_join_bookmarks()
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('bookmarks-received', account=self._account))
|
||||
|
||||
def store_difference(self, bookmarks: List) -> None:
|
||||
if self.nativ_bookmarks_used:
|
||||
retract, add_or_modify = self._determine_changed_bookmarks(
|
||||
bookmarks, self._bookmarks)
|
||||
|
||||
for bookmark in retract:
|
||||
self.remove(bookmark.jid)
|
||||
|
||||
if add_or_modify:
|
||||
self.store_bookmarks(add_or_modify)
|
||||
self._bookmarks = self._convert_to_dict(bookmarks)
|
||||
|
||||
else:
|
||||
self._bookmarks = self._convert_to_dict(bookmarks)
|
||||
self.store_bookmarks()
|
||||
|
||||
def store_bookmarks(self, bookmarks: list = None) -> None:
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
|
||||
if bookmarks is None or not self.nativ_bookmarks_used:
|
||||
bookmarks = self._bookmarks.values()
|
||||
|
||||
self._nbxmpp(self._bookmark_module()).store_bookmarks(bookmarks)
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('bookmarks-received', account=self._account))
|
||||
|
||||
def _join_with_timeout(self, bookmarks: List[Any]) -> None:
|
||||
self._join_timeouts.pop(0)
|
||||
self.auto_join_bookmarks(bookmarks)
|
||||
|
||||
def auto_join_bookmarks(self,
|
||||
bookmarks: Optional[List[Any]] = None) -> None:
|
||||
if bookmarks is None:
|
||||
bookmarks = self._bookmarks.values()
|
||||
|
||||
for bookmark in bookmarks:
|
||||
if bookmark.autojoin:
|
||||
# Only join non-opened groupchats. Opened one are already
|
||||
# auto-joined on re-connection
|
||||
if bookmark.jid not in app.gc_connected[self._account]:
|
||||
# we are not already connected
|
||||
self._log.info('Autojoin Bookmark: %s', bookmark.jid)
|
||||
minimize = app.settings.get_group_chat_setting(
|
||||
self._account,
|
||||
bookmark.jid,
|
||||
'minimize_on_autojoin')
|
||||
app.interface.join_groupchat(self._account,
|
||||
str(bookmark.jid),
|
||||
minimized=minimize)
|
||||
|
||||
def modify(self, jid: str, **kwargs: Dict[str, str]) -> None:
|
||||
bookmark = self._bookmarks.get(jid)
|
||||
if bookmark is None:
|
||||
return
|
||||
|
||||
new_bookmark = bookmark._replace(**kwargs)
|
||||
if new_bookmark == bookmark:
|
||||
# No change happened
|
||||
return
|
||||
self._log.info('Modify bookmark: %s %s', jid, kwargs)
|
||||
self._bookmarks[jid] = new_bookmark
|
||||
|
||||
self.store_bookmarks([new_bookmark])
|
||||
|
||||
def add_or_modify(self, jid: str, **kwargs: Dict[str, str]) -> None:
|
||||
bookmark = self._bookmarks.get(jid)
|
||||
if bookmark is not None:
|
||||
self.modify(jid, **kwargs)
|
||||
return
|
||||
|
||||
new_bookmark = BookmarkData(jid=jid, **kwargs)
|
||||
self._bookmarks[jid] = new_bookmark
|
||||
self._log.info('Add new bookmark: %s', new_bookmark)
|
||||
|
||||
self.store_bookmarks([new_bookmark])
|
||||
|
||||
def remove(self, jid: JID, publish: bool = True) -> None:
|
||||
removed = self._bookmarks.pop(jid, False)
|
||||
if not removed:
|
||||
return
|
||||
if publish:
|
||||
if self.nativ_bookmarks_used:
|
||||
self._nbxmpp('NativeBookmarks').retract_bookmark(str(jid))
|
||||
else:
|
||||
self.store_bookmarks()
|
||||
|
||||
@staticmethod
|
||||
def _determine_changed_bookmarks(
|
||||
new_bookmarks: List[BookmarkData],
|
||||
old_bookmarks: Dict[str, BookmarkData]) -> Tuple[
|
||||
List[BookmarkData], List[BookmarkData]]:
|
||||
|
||||
new_jids = [bookmark.jid for bookmark in new_bookmarks]
|
||||
new_bookmarks = set(new_bookmarks)
|
||||
old_bookmarks = set(old_bookmarks.values())
|
||||
|
||||
retract = []
|
||||
add_or_modify = []
|
||||
changed_bookmarks = new_bookmarks.symmetric_difference(old_bookmarks)
|
||||
|
||||
for bookmark in changed_bookmarks:
|
||||
if bookmark.jid not in new_jids:
|
||||
retract.append(bookmark)
|
||||
if bookmark in new_bookmarks:
|
||||
add_or_modify.append(bookmark)
|
||||
return retract, add_or_modify
|
||||
|
||||
def get_name_from_bookmark(self, jid: str) -> str:
|
||||
bookmark = self._bookmarks.get(jid)
|
||||
if bookmark is None:
|
||||
return ''
|
||||
return bookmark.name
|
||||
|
||||
def is_bookmark(self, jid: str) -> bool:
|
||||
return jid in self._bookmarks
|
||||
|
||||
def _remove_timeouts(self):
|
||||
for _id in self._join_timeouts:
|
||||
GLib.source_remove(_id)
|
||||
|
||||
def cleanup(self):
|
||||
self._remove_timeouts()
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return Bookmarks(*args, **kwargs), 'Bookmarks'
|
719
gajim/common/modules/bytestream.py
Normal file
719
gajim/common/modules/bytestream.py
Normal file
|
@ -0,0 +1,719 @@
|
|||
# Copyright (C) 2006 Dimitur Kirov <dkirov AT gmail.com>
|
||||
# Junglecow J <junglecow AT gmail.com>
|
||||
# Copyright (C) 2006-2007 Tomasz Melcer <liori AT exroot.org>
|
||||
# Travis Shirk <travis AT pobox.com>
|
||||
# Nikos Kouremenos <kourem AT gmail.com>
|
||||
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
|
||||
# Copyright (C) 2007 Julien Pivotto <roidelapluie AT gmail.com>
|
||||
# Copyright (C) 2007-2008 Brendan Taylor <whateley AT gmail.com>
|
||||
# Jean-Marie Traissard <jim AT lapin.org>
|
||||
# Stephan Erb <steve-e AT h3c.de>
|
||||
# Copyright (C) 2008 Jonathan Schleifer <js-gajim AT webkeks.org>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
import socket
|
||||
import logging
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
from gi.repository import GLib
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common import helpers
|
||||
from gajim.common import jingle_xtls
|
||||
from gajim.common.file_props import FilesProp
|
||||
from gajim.common.socks5 import Socks5SenderClient
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
log = logging.getLogger('gajim.c.m.bytestream')
|
||||
|
||||
def is_transfer_paused(file_props):
|
||||
if file_props.stopped:
|
||||
return False
|
||||
if file_props.completed:
|
||||
return False
|
||||
if file_props.disconnect_cb:
|
||||
return False
|
||||
return file_props.paused
|
||||
|
||||
def is_transfer_active(file_props):
|
||||
if file_props.stopped:
|
||||
return False
|
||||
if file_props.completed:
|
||||
return False
|
||||
if not file_props.started:
|
||||
return False
|
||||
if file_props.paused:
|
||||
return True
|
||||
return not file_props.paused
|
||||
|
||||
def is_transfer_stopped(file_props):
|
||||
if not file_props:
|
||||
return True
|
||||
if file_props.error:
|
||||
return True
|
||||
if file_props.completed:
|
||||
return True
|
||||
if not file_props.stopped:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class Bytestream(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='iq',
|
||||
typ='result',
|
||||
ns=Namespace.BYTESTREAM,
|
||||
callback=self._on_bytestream_result),
|
||||
StanzaHandler(name='iq',
|
||||
typ='error',
|
||||
ns=Namespace.BYTESTREAM,
|
||||
callback=self._on_bytestream_error),
|
||||
StanzaHandler(name='iq',
|
||||
typ='set',
|
||||
ns=Namespace.BYTESTREAM,
|
||||
callback=self._on_bytestream_set),
|
||||
StanzaHandler(name='iq',
|
||||
typ='result',
|
||||
callback=self._on_result),
|
||||
]
|
||||
|
||||
self.no_gupnp_reply_id = None
|
||||
self.ok_id = None
|
||||
self.fail_id = None
|
||||
|
||||
def pass_disco(self, info):
|
||||
if Namespace.BYTESTREAM not in info.features:
|
||||
return
|
||||
if app.settings.get_account_setting(self._account, 'use_ft_proxies'):
|
||||
log.info('Discovered proxy: %s', info.jid)
|
||||
our_fjid = self._con.get_own_jid()
|
||||
testit = app.settings.get_account_setting(
|
||||
self._account, 'test_ft_proxies_on_startup')
|
||||
app.proxy65_manager.resolve(
|
||||
info.jid, self._con.connection, str(our_fjid),
|
||||
default=self._account, testit=testit)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _ft_get_receiver_jid(self, file_props):
|
||||
if self._account == 'Local':
|
||||
return file_props.receiver.jid
|
||||
return file_props.receiver.jid + '/' + file_props.receiver.resource
|
||||
|
||||
def _ft_get_from(self, iq_obj):
|
||||
if self._account == 'Local':
|
||||
return iq_obj.getFrom()
|
||||
return helpers.get_full_jid_from_iq(iq_obj)
|
||||
|
||||
def _ft_get_streamhost_jid_attr(self, streamhost):
|
||||
if self._account == 'Local':
|
||||
return streamhost.getAttr('jid')
|
||||
return helpers.parse_jid(streamhost.getAttr('jid'))
|
||||
|
||||
def send_file_approval(self, file_props):
|
||||
"""
|
||||
Send iq, confirming that we want to download the file
|
||||
"""
|
||||
# user response to ConfirmationDialog may come after we've disconnected
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
|
||||
# file transfer initiated by a jingle session
|
||||
log.info("send_file_approval: jingle session accept")
|
||||
|
||||
session = self._con.get_module('Jingle').get_jingle_session(
|
||||
file_props.sender, file_props.sid)
|
||||
if not session:
|
||||
return
|
||||
content = None
|
||||
for content_ in session.contents.values():
|
||||
if content_.transport.sid == file_props.transport_sid:
|
||||
content = content_
|
||||
break
|
||||
|
||||
if not content:
|
||||
return
|
||||
|
||||
if not session.accepted:
|
||||
content = session.get_content('file', content.name)
|
||||
if content.use_security:
|
||||
fingerprint = content.x509_fingerprint
|
||||
if not jingle_xtls.check_cert(
|
||||
app.get_jid_without_resource(file_props.sender),
|
||||
fingerprint):
|
||||
id_ = jingle_xtls.send_cert_request(
|
||||
self._con, file_props.sender)
|
||||
jingle_xtls.key_exchange_pend(id_,
|
||||
content.on_cert_received, [])
|
||||
return
|
||||
session.approve_session()
|
||||
|
||||
session.approve_content('file', content.name)
|
||||
|
||||
def send_file_rejection(self, file_props):
|
||||
"""
|
||||
Inform sender that we refuse to download the file
|
||||
|
||||
typ is used when code = '400', in this case typ can be 'stream' for
|
||||
invalid stream or 'profile' for invalid profile
|
||||
"""
|
||||
# user response to ConfirmationDialog may come after we've disconnected
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
|
||||
for session in self._con.get_module('Jingle').get_jingle_sessions(
|
||||
None, file_props.sid):
|
||||
session.cancel_session()
|
||||
|
||||
def send_success_connect_reply(self, streamhost):
|
||||
"""
|
||||
Send reply to the initiator of FT that we made a connection
|
||||
"""
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
if streamhost is None:
|
||||
return
|
||||
iq = nbxmpp.Iq(to=streamhost['initiator'],
|
||||
typ='result',
|
||||
frm=streamhost['target'])
|
||||
iq.setAttr('id', streamhost['id'])
|
||||
query = iq.setTag('query', namespace=Namespace.BYTESTREAM)
|
||||
stream_tag = query.setTag('streamhost-used')
|
||||
stream_tag.setAttr('jid', streamhost['jid'])
|
||||
self._con.connection.send(iq)
|
||||
|
||||
def stop_all_active_file_transfers(self, contact):
|
||||
"""
|
||||
Stop all active transfer to or from the given contact
|
||||
"""
|
||||
for file_props in FilesProp.getAllFileProp():
|
||||
if is_transfer_stopped(file_props):
|
||||
continue
|
||||
receiver_jid = file_props.receiver
|
||||
if contact.get_full_jid() == receiver_jid:
|
||||
file_props.error = -5
|
||||
self.remove_transfer(file_props)
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('file-request-error',
|
||||
conn=self._con,
|
||||
jid=app.get_jid_without_resource(contact.jid),
|
||||
file_props=file_props,
|
||||
error_msg=''))
|
||||
sender_jid = file_props.sender
|
||||
if contact.get_full_jid() == sender_jid:
|
||||
file_props.error = -3
|
||||
self.remove_transfer(file_props)
|
||||
|
||||
def remove_all_transfers(self):
|
||||
"""
|
||||
Stop and remove all active connections from the socks5 pool
|
||||
"""
|
||||
for file_props in FilesProp.getAllFileProp():
|
||||
self.remove_transfer(file_props)
|
||||
|
||||
def remove_transfer(self, file_props):
|
||||
if file_props is None:
|
||||
return
|
||||
self.disconnect_transfer(file_props)
|
||||
|
||||
@staticmethod
|
||||
def disconnect_transfer(file_props):
|
||||
if file_props is None:
|
||||
return
|
||||
if file_props.hash_:
|
||||
app.socks5queue.remove_sender(file_props.hash_)
|
||||
|
||||
if file_props.streamhosts:
|
||||
for host in file_props.streamhosts:
|
||||
if 'idx' in host and host['idx'] > 0:
|
||||
app.socks5queue.remove_receiver(host['idx'])
|
||||
app.socks5queue.remove_sender(host['idx'])
|
||||
|
||||
def _send_socks5_info(self, file_props):
|
||||
"""
|
||||
Send iq for the present streamhosts and proxies
|
||||
"""
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
receiver = file_props.receiver
|
||||
sender = file_props.sender
|
||||
|
||||
sha_str = helpers.get_auth_sha(file_props.sid, sender, receiver)
|
||||
file_props.sha_str = sha_str
|
||||
|
||||
port = app.settings.get('file_transfers_port')
|
||||
listener = app.socks5queue.start_listener(
|
||||
port,
|
||||
sha_str,
|
||||
self._result_socks5_sid, file_props)
|
||||
if not listener:
|
||||
file_props.error = -5
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('file-request-error',
|
||||
conn=self._con,
|
||||
jid=app.get_jid_without_resource(receiver),
|
||||
file_props=file_props,
|
||||
error_msg=''))
|
||||
self._connect_error(file_props.sid,
|
||||
error='not-acceptable',
|
||||
error_type='modify')
|
||||
else:
|
||||
iq = nbxmpp.Iq(to=receiver, typ='set')
|
||||
file_props.request_id = 'id_' + file_props.sid
|
||||
iq.setID(file_props.request_id)
|
||||
query = iq.setTag('query', namespace=Namespace.BYTESTREAM)
|
||||
query.setAttr('sid', file_props.sid)
|
||||
|
||||
self._add_addiditional_streamhosts_to_query(query, file_props)
|
||||
self._add_local_ips_as_streamhosts_to_query(query, file_props)
|
||||
self._add_proxy_streamhosts_to_query(query, file_props)
|
||||
self._add_upnp_igd_as_streamhost_to_query(query, file_props, iq)
|
||||
# Upnp-igd is asynchronous, so it will send the iq itself
|
||||
|
||||
@staticmethod
|
||||
def _add_streamhosts_to_query(query, sender, port, hosts):
|
||||
for host in hosts:
|
||||
streamhost = nbxmpp.Node(tag='streamhost')
|
||||
query.addChild(node=streamhost)
|
||||
streamhost.setAttr('port', str(port))
|
||||
streamhost.setAttr('host', host)
|
||||
streamhost.setAttr('jid', sender)
|
||||
|
||||
def _add_local_ips_as_streamhosts_to_query(self, query, file_props):
|
||||
if not app.settings.get_account_setting(self._account,
|
||||
'ft_send_local_ips'):
|
||||
return
|
||||
|
||||
my_ip = self._con.local_address
|
||||
if my_ip is None:
|
||||
log.warning('No local address available')
|
||||
return
|
||||
|
||||
try:
|
||||
# The ip we're connected to server with
|
||||
my_ips = [my_ip]
|
||||
# all IPs from local DNS
|
||||
for addr in socket.getaddrinfo(socket.gethostname(), None):
|
||||
if (not addr[4][0] in my_ips and
|
||||
not addr[4][0].startswith('127') and
|
||||
not addr[4][0] == '::1'):
|
||||
my_ips.append(addr[4][0])
|
||||
|
||||
sender = file_props.sender
|
||||
port = app.settings.get('file_transfers_port')
|
||||
self._add_streamhosts_to_query(query, sender, port, my_ips)
|
||||
except socket.gaierror:
|
||||
from gajim.common.connection_handlers_events import InformationEvent
|
||||
app.nec.push_incoming_event(
|
||||
InformationEvent(None, dialog_name='wrong-host'))
|
||||
|
||||
def _add_addiditional_streamhosts_to_query(self, query, file_props):
|
||||
sender = file_props.sender
|
||||
port = app.settings.get('file_transfers_port')
|
||||
ft_add_hosts_to_send = app.settings.get('ft_add_hosts_to_send')
|
||||
add_hosts = []
|
||||
if ft_add_hosts_to_send:
|
||||
add_hosts = [e.strip() for e in ft_add_hosts_to_send.split(',')]
|
||||
else:
|
||||
add_hosts = []
|
||||
self._add_streamhosts_to_query(query, sender, port, add_hosts)
|
||||
|
||||
def _add_upnp_igd_as_streamhost_to_query(self, query, file_props, iq):
|
||||
my_ip = self._con.local_address
|
||||
if my_ip is None or not app.is_installed('UPNP'):
|
||||
log.warning('No local address available')
|
||||
self._con.connection.send(iq)
|
||||
return
|
||||
|
||||
# check if we are connected with an IPv4 address
|
||||
try:
|
||||
socket.inet_aton(my_ip)
|
||||
except socket.error:
|
||||
self._con.connection.send(iq)
|
||||
return
|
||||
|
||||
def ip_is_local(ip):
|
||||
if '.' not in ip:
|
||||
# it's an IPv6
|
||||
return True
|
||||
ip_s = ip.split('.')
|
||||
ip_l = int(ip_s[0])<<24 | int(ip_s[1])<<16 | int(ip_s[2])<<8 | \
|
||||
int(ip_s[3])
|
||||
# 10/8
|
||||
if ip_l & (255<<24) == 10<<24:
|
||||
return True
|
||||
# 172.16/12
|
||||
if ip_l & (255<<24 | 240<<16) == (172<<24 | 16<<16):
|
||||
return True
|
||||
# 192.168
|
||||
if ip_l & (255<<24 | 255<<16) == (192<<24 | 168<<16):
|
||||
return True
|
||||
return False
|
||||
|
||||
if not ip_is_local(my_ip):
|
||||
self.connection.send(iq)
|
||||
return
|
||||
|
||||
self.no_gupnp_reply_id = 0
|
||||
|
||||
def cleanup_gupnp():
|
||||
if self.no_gupnp_reply_id:
|
||||
GLib.source_remove(self.no_gupnp_reply_id)
|
||||
self.no_gupnp_reply_id = 0
|
||||
app.gupnp_igd.disconnect(self.ok_id)
|
||||
app.gupnp_igd.disconnect(self.fail_id)
|
||||
|
||||
def success(_gupnp, _proto, ext_ip, _re, ext_port,
|
||||
local_ip, local_port, _desc):
|
||||
log.debug('Got GUPnP-IGD answer: external: %s:%s, internal: %s:%s',
|
||||
ext_ip, ext_port, local_ip, local_port)
|
||||
if local_port != app.settings.get('file_transfers_port'):
|
||||
sender = file_props.sender
|
||||
receiver = file_props.receiver
|
||||
sha_str = helpers.get_auth_sha(file_props.sid,
|
||||
sender,
|
||||
receiver)
|
||||
listener = app.socks5queue.start_listener(
|
||||
local_port,
|
||||
sha_str,
|
||||
self._result_socks5_sid,
|
||||
file_props.sid)
|
||||
if listener:
|
||||
self._add_streamhosts_to_query(query,
|
||||
sender,
|
||||
ext_port,
|
||||
[ext_ip])
|
||||
else:
|
||||
self._add_streamhosts_to_query(query,
|
||||
file_props.sender,
|
||||
ext_port,
|
||||
[ext_ip])
|
||||
self._con.connection.send(iq)
|
||||
cleanup_gupnp()
|
||||
|
||||
def fail(_gupnp, error, _proto, _ext_ip, _local_ip, _local_port, _desc):
|
||||
log.debug('Got GUPnP-IGD error: %s', error)
|
||||
self._con.connection.send(iq)
|
||||
cleanup_gupnp()
|
||||
|
||||
def no_upnp_reply():
|
||||
log.debug('Got not GUPnP-IGD answer')
|
||||
# stop trying to use it
|
||||
app.disable_dependency('UPNP')
|
||||
self.no_gupnp_reply_id = 0
|
||||
self._con.connection.send(iq)
|
||||
cleanup_gupnp()
|
||||
return False
|
||||
|
||||
|
||||
self.ok_id = app.gupnp_igd.connect('mapped-external-port', success)
|
||||
self.fail_id = app.gupnp_igd.connect('error-mapping-port', fail)
|
||||
|
||||
port = app.settings.get('file_transfers_port')
|
||||
self.no_gupnp_reply_id = GLib.timeout_add_seconds(10, no_upnp_reply)
|
||||
app.gupnp_igd.add_port('TCP',
|
||||
0,
|
||||
my_ip,
|
||||
port,
|
||||
3600,
|
||||
'Gajim file transfer')
|
||||
|
||||
def _add_proxy_streamhosts_to_query(self, query, file_props):
|
||||
proxyhosts = self._get_file_transfer_proxies_from_config(file_props)
|
||||
if proxyhosts:
|
||||
file_props.proxy_receiver = file_props.receiver
|
||||
file_props.proxy_sender = file_props.sender
|
||||
file_props.proxyhosts = proxyhosts
|
||||
|
||||
for proxyhost in proxyhosts:
|
||||
self._add_streamhosts_to_query(query,
|
||||
proxyhost['jid'],
|
||||
proxyhost['port'],
|
||||
[proxyhost['host']])
|
||||
|
||||
def _get_file_transfer_proxies_from_config(self, file_props):
|
||||
configured_proxies = app.settings.get_account_setting(
|
||||
self._account, 'file_transfer_proxies')
|
||||
shall_use_proxies = app.settings.get_account_setting(
|
||||
self._account, 'use_ft_proxies')
|
||||
if shall_use_proxies:
|
||||
proxyhost_dicts = []
|
||||
proxies = []
|
||||
if configured_proxies:
|
||||
proxies = [item.strip() for item in
|
||||
configured_proxies.split(',')]
|
||||
default_proxy = app.proxy65_manager.get_default_for_name(
|
||||
self._account)
|
||||
if default_proxy:
|
||||
# add/move default proxy at top of the others
|
||||
if default_proxy in proxies:
|
||||
proxies.remove(default_proxy)
|
||||
proxies.insert(0, default_proxy)
|
||||
|
||||
for proxy in proxies:
|
||||
(host, _port, jid) = app.proxy65_manager.get_proxy(
|
||||
proxy, self._account)
|
||||
if not host:
|
||||
continue
|
||||
host_dict = {
|
||||
'state': 0,
|
||||
'target': file_props.receiver,
|
||||
'id': file_props.sid,
|
||||
'sid': file_props.sid,
|
||||
'initiator': proxy,
|
||||
'host': host,
|
||||
'port': str(_port),
|
||||
'jid': jid
|
||||
}
|
||||
proxyhost_dicts.append(host_dict)
|
||||
return proxyhost_dicts
|
||||
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _result_socks5_sid(sid, hash_id):
|
||||
"""
|
||||
Store the result of SHA message from auth
|
||||
"""
|
||||
file_props = FilesProp.getFilePropBySid(sid)
|
||||
file_props.hash_ = hash_id
|
||||
|
||||
def _connect_error(self, sid, error, error_type, msg=None):
|
||||
"""
|
||||
Called when there is an error establishing BS connection, or when
|
||||
connection is rejected
|
||||
"""
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
file_props = FilesProp.getFileProp(self._account, sid)
|
||||
if file_props is None:
|
||||
log.error('can not send iq error on failed transfer')
|
||||
return
|
||||
if file_props.type_ == 's':
|
||||
to = file_props.receiver
|
||||
else:
|
||||
to = file_props.sender
|
||||
iq = nbxmpp.Iq(to=to, typ='error')
|
||||
iq.setAttr('id', file_props.request_id)
|
||||
err = iq.setTag('error')
|
||||
err.setAttr('type', error_type)
|
||||
err.setTag(error, namespace=Namespace.STANZAS)
|
||||
self._con.connection.send(iq)
|
||||
if msg:
|
||||
self.disconnect_transfer(file_props)
|
||||
file_props.error = -3
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('file-request-error',
|
||||
conn=self._con,
|
||||
jid=app.get_jid_without_resource(to),
|
||||
file_props=file_props,
|
||||
error_msg=msg))
|
||||
|
||||
def _proxy_auth_ok(self, proxy):
|
||||
"""
|
||||
Called after authentication to proxy server
|
||||
"""
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
file_props = FilesProp.getFileProp(self._account, proxy['sid'])
|
||||
iq = nbxmpp.Iq(to=proxy['initiator'], typ='set')
|
||||
auth_id = "au_" + proxy['sid']
|
||||
iq.setID(auth_id)
|
||||
query = iq.setTag('query', namespace=Namespace.BYTESTREAM)
|
||||
query.setAttr('sid', proxy['sid'])
|
||||
activate = query.setTag('activate')
|
||||
activate.setData(file_props.proxy_receiver)
|
||||
iq.setID(auth_id)
|
||||
self._con.connection.send(iq)
|
||||
|
||||
def _on_bytestream_error(self, _con, iq_obj, _properties):
|
||||
id_ = iq_obj.getAttr('id')
|
||||
frm = helpers.get_full_jid_from_iq(iq_obj)
|
||||
query = iq_obj.getTag('query')
|
||||
app.proxy65_manager.error_cb(frm, query)
|
||||
jid = helpers.get_jid_from_iq(iq_obj)
|
||||
id_ = id_[3:]
|
||||
file_props = FilesProp.getFilePropBySid(id_)
|
||||
if not file_props:
|
||||
return
|
||||
file_props.error = -4
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('file-request-error',
|
||||
conn=self._con,
|
||||
jid=app.get_jid_without_resource(jid),
|
||||
file_props=file_props,
|
||||
error_msg=''))
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _on_bytestream_set(self, con, iq_obj, _properties):
|
||||
target = iq_obj.getAttr('to')
|
||||
id_ = iq_obj.getAttr('id')
|
||||
query = iq_obj.getTag('query')
|
||||
sid = query.getAttr('sid')
|
||||
file_props = FilesProp.getFileProp(self._account, sid)
|
||||
streamhosts = []
|
||||
for item in query.getChildren():
|
||||
if item.getName() == 'streamhost':
|
||||
host_dict = {
|
||||
'state': 0,
|
||||
'target': target,
|
||||
'id': id_,
|
||||
'sid': sid,
|
||||
'initiator': self._ft_get_from(iq_obj)
|
||||
}
|
||||
for attr in item.getAttrs():
|
||||
host_dict[attr] = item.getAttr(attr)
|
||||
if 'host' not in host_dict:
|
||||
continue
|
||||
if 'jid' not in host_dict:
|
||||
continue
|
||||
if 'port' not in host_dict:
|
||||
continue
|
||||
streamhosts.append(host_dict)
|
||||
file_props = FilesProp.getFilePropBySid(sid)
|
||||
if file_props is not None:
|
||||
if file_props.type_ == 's': # FIXME: remove fast xmlns
|
||||
# only psi do this
|
||||
if file_props.streamhosts:
|
||||
file_props.streamhosts.extend(streamhosts)
|
||||
else:
|
||||
file_props.streamhosts = streamhosts
|
||||
app.socks5queue.connect_to_hosts(
|
||||
self._account,
|
||||
sid,
|
||||
self.send_success_connect_reply,
|
||||
None)
|
||||
raise nbxmpp.NodeProcessed
|
||||
else:
|
||||
log.warning('Gajim got streamhosts for unknown transfer. '
|
||||
'Ignoring it.')
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
file_props.streamhosts = streamhosts
|
||||
def _connection_error(sid):
|
||||
self._connect_error(sid,
|
||||
'item-not-found',
|
||||
'cancel',
|
||||
msg='Could not connect to given hosts')
|
||||
if file_props.type_ == 'r':
|
||||
app.socks5queue.connect_to_hosts(
|
||||
self._account,
|
||||
sid,
|
||||
self.send_success_connect_reply,
|
||||
_connection_error)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _on_result(self, _con, iq_obj, _properties):
|
||||
# if we want to respect xep-0065 we have to check for proxy
|
||||
# activation result in any result iq
|
||||
real_id = iq_obj.getAttr('id')
|
||||
if real_id is None:
|
||||
log.warning('Invalid IQ without id attribute:\n%s', iq_obj)
|
||||
raise nbxmpp.NodeProcessed
|
||||
if real_id is None or not real_id.startswith('au_'):
|
||||
return
|
||||
frm = self._ft_get_from(iq_obj)
|
||||
id_ = real_id[3:]
|
||||
file_props = FilesProp.getFilePropByTransportSid(self._account, id_)
|
||||
if file_props.streamhost_used:
|
||||
for host in file_props.proxyhosts:
|
||||
if host['initiator'] == frm and 'idx' in host:
|
||||
app.socks5queue.activate_proxy(host['idx'])
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _on_bytestream_result(self, con, iq_obj, _properties):
|
||||
frm = self._ft_get_from(iq_obj)
|
||||
real_id = iq_obj.getAttr('id')
|
||||
query = iq_obj.getTag('query')
|
||||
app.proxy65_manager.resolve_result(frm, query)
|
||||
|
||||
try:
|
||||
streamhost = query.getTag('streamhost-used')
|
||||
except Exception: # this bytestream result is not what we need
|
||||
pass
|
||||
id_ = real_id[3:]
|
||||
file_props = FilesProp.getFileProp(self._account, id_)
|
||||
if file_props is None:
|
||||
raise nbxmpp.NodeProcessed
|
||||
if streamhost is None:
|
||||
# proxy approves the activate query
|
||||
if real_id.startswith('au_'):
|
||||
if file_props.streamhost_used is False:
|
||||
raise nbxmpp.NodeProcessed
|
||||
if not file_props.proxyhosts:
|
||||
raise nbxmpp.NodeProcessed
|
||||
for host in file_props.proxyhosts:
|
||||
if host['initiator'] == frm and \
|
||||
query.getAttr('sid') == file_props.sid:
|
||||
app.socks5queue.activate_proxy(host['idx'])
|
||||
break
|
||||
raise nbxmpp.NodeProcessed
|
||||
jid = self._ft_get_streamhost_jid_attr(streamhost)
|
||||
if file_props.streamhost_used is True:
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
if real_id.startswith('au_'):
|
||||
if file_props.stopped:
|
||||
self.remove_transfer(file_props)
|
||||
else:
|
||||
app.socks5queue.send_file(file_props, self._account, 'server')
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
proxy = None
|
||||
if file_props.proxyhosts:
|
||||
for proxyhost in file_props.proxyhosts:
|
||||
if proxyhost['jid'] == jid:
|
||||
proxy = proxyhost
|
||||
|
||||
if file_props.stopped:
|
||||
self.remove_transfer(file_props)
|
||||
raise nbxmpp.NodeProcessed
|
||||
if proxy is not None:
|
||||
file_props.streamhost_used = True
|
||||
file_props.streamhosts.append(proxy)
|
||||
file_props.is_a_proxy = True
|
||||
idx = app.socks5queue.idx
|
||||
sender = Socks5SenderClient(app.idlequeue,
|
||||
idx,
|
||||
app.socks5queue,
|
||||
_sock=None,
|
||||
host=str(proxy['host']),
|
||||
port=int(proxy['port']),
|
||||
fingerprint=None,
|
||||
connected=False,
|
||||
file_props=file_props)
|
||||
sender.streamhost = proxy
|
||||
app.socks5queue.add_sockobj(self._account, sender)
|
||||
proxy['idx'] = sender.queue_idx
|
||||
app.socks5queue.on_success[file_props.sid] = self._proxy_auth_ok
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
if file_props.stopped:
|
||||
self.remove_transfer(file_props)
|
||||
else:
|
||||
app.socks5queue.send_file(file_props, self._account, 'server')
|
||||
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return Bytestream(*args, **kwargs), 'Bytestream'
|
232
gajim/common/modules/caps.py
Normal file
232
gajim/common/modules/caps.py
Normal file
|
@ -0,0 +1,232 @@
|
|||
# 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'
|
44
gajim/common/modules/carbons.py
Normal file
44
gajim/common/modules/carbons.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
# 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-0280: Message Carbons
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class Carbons(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.supported = False
|
||||
|
||||
def pass_disco(self, info):
|
||||
if Namespace.CARBONS not in info.features:
|
||||
return
|
||||
|
||||
self.supported = True
|
||||
self._log.info('Discovered carbons: %s', info.jid)
|
||||
|
||||
iq = nbxmpp.Iq('set')
|
||||
iq.setTag('enable', namespace=Namespace.CARBONS)
|
||||
self._log.info('Activate')
|
||||
self._con.connection.send(iq)
|
||||
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return Carbons(*args, **kwargs), 'Carbons'
|
123
gajim/common/modules/chat_markers.py
Normal file
123
gajim/common/modules/chat_markers.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
# 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/>.
|
||||
|
||||
# Chat Markers (XEP-0333)
|
||||
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.structs import OutgoingMessage
|
||||
|
||||
|
||||
class ChatMarkers(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'ChatMarkers'
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='message',
|
||||
callback=self._process_chat_marker,
|
||||
ns=Namespace.CHATMARKERS,
|
||||
priority=47),
|
||||
]
|
||||
|
||||
def _process_chat_marker(self, _con, _stanza, properties):
|
||||
if not properties.is_marker or not properties.marker.is_displayed:
|
||||
return
|
||||
|
||||
if properties.type.is_error:
|
||||
return
|
||||
|
||||
if properties.type.is_groupchat:
|
||||
manager = self._con.get_module('MUC').get_manager()
|
||||
muc_data = manager.get(properties.muc_jid)
|
||||
if muc_data is None:
|
||||
return
|
||||
|
||||
if properties.muc_nickname != muc_data.nick:
|
||||
return
|
||||
|
||||
self._raise_event('read-state-sync', properties)
|
||||
return
|
||||
|
||||
if properties.is_carbon_message and properties.carbon.is_sent:
|
||||
self._raise_event('read-state-sync', properties)
|
||||
return
|
||||
|
||||
if properties.is_mam_message:
|
||||
if properties.from_.bareMatch(self._con.get_own_jid()):
|
||||
return
|
||||
|
||||
self._raise_event('displayed-received', properties)
|
||||
|
||||
def _raise_event(self, name, properties):
|
||||
self._log.info('%s: %s %s',
|
||||
name,
|
||||
properties.jid,
|
||||
properties.marker.id)
|
||||
|
||||
jid = properties.jid
|
||||
if not properties.is_muc_pm and not properties.type.is_groupchat:
|
||||
jid = properties.jid.bare
|
||||
|
||||
app.storage.archive.set_marker(
|
||||
app.get_jid_from_account(self._account),
|
||||
jid,
|
||||
properties.marker.id,
|
||||
'displayed')
|
||||
|
||||
app.nec.push_outgoing_event(
|
||||
NetworkEvent(name,
|
||||
account=self._account,
|
||||
jid=jid,
|
||||
properties=properties,
|
||||
type=properties.type,
|
||||
is_muc_pm=properties.is_muc_pm,
|
||||
marker_id=properties.marker.id))
|
||||
|
||||
def _send_marker(self, contact, marker, id_, type_):
|
||||
jid = contact.jid
|
||||
if contact.is_pm_contact:
|
||||
jid = app.get_jid_without_resource(contact.jid)
|
||||
|
||||
if type_ in ('gc', 'pm'):
|
||||
if not app.settings.get_group_chat_setting(
|
||||
self._account, jid, 'send_marker'):
|
||||
return
|
||||
else:
|
||||
if not app.settings.get_contact_setting(
|
||||
self._account, jid, 'send_marker'):
|
||||
return
|
||||
|
||||
typ = 'groupchat' if type_ == 'gc' else 'chat'
|
||||
message = OutgoingMessage(account=self._account,
|
||||
contact=contact,
|
||||
message=None,
|
||||
type_=typ,
|
||||
marker=(marker, id_),
|
||||
play_sound=False)
|
||||
self._con.send_message(message)
|
||||
self._log.info('Send %s: %s', marker, contact.jid)
|
||||
|
||||
def send_displayed_marker(self, contact, id_, type_):
|
||||
self._send_marker(contact, 'displayed', id_, str(type_))
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return ChatMarkers(*args, **kwargs), 'ChatMarkers'
|
358
gajim/common/modules/chatstates.py
Normal file
358
gajim/common/modules/chatstates.py
Normal file
|
@ -0,0 +1,358 @@
|
|||
# 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-0085: Chat State Notifications
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict # pylint: disable=unused-import
|
||||
from typing import List # pylint: disable=unused-import
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
|
||||
import time
|
||||
from functools import wraps
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
from gi.repository import GLib
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.const import Chatstate as State
|
||||
from gajim.common.structs import OutgoingMessage
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
from gajim.common.types import ContactT
|
||||
from gajim.common.types import ConnectionT
|
||||
|
||||
|
||||
INACTIVE_AFTER = 60
|
||||
PAUSED_AFTER = 10
|
||||
|
||||
|
||||
def ensure_enabled(func):
|
||||
@wraps(func)
|
||||
def func_wrapper(self, *args, **kwargs):
|
||||
if not self.enabled:
|
||||
return None
|
||||
return func(self, *args, **kwargs)
|
||||
return func_wrapper
|
||||
|
||||
|
||||
class Chatstate(BaseModule):
|
||||
def __init__(self, con: ConnectionT) -> None:
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='presence',
|
||||
callback=self._presence_received),
|
||||
StanzaHandler(name='message',
|
||||
callback=self._process_chatstate,
|
||||
ns=Namespace.CHATSTATES,
|
||||
priority=46),
|
||||
]
|
||||
|
||||
# Our current chatstate with a specific contact
|
||||
self._chatstates = {} # type: Dict[str, State]
|
||||
|
||||
self._last_keyboard_activity = {} # type: Dict[str, float]
|
||||
self._last_mouse_activity = {} # type: Dict[str, float]
|
||||
self._timeout_id = None
|
||||
self._delay_timeout_ids = {} # type: Dict[str, str]
|
||||
self._blocked = [] # type: List[str]
|
||||
self._enabled = False
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return self._enabled
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, value):
|
||||
if self._enabled == value:
|
||||
return
|
||||
self._log.info('Chatstate module %s',
|
||||
'enabled' if value else 'disabled')
|
||||
self._enabled = value
|
||||
|
||||
if value:
|
||||
self._timeout_id = GLib.timeout_add_seconds(
|
||||
2, self._check_last_interaction)
|
||||
else:
|
||||
self.cleanup()
|
||||
self._chatstates = {}
|
||||
self._last_keyboard_activity = {}
|
||||
self._last_mouse_activity = {}
|
||||
self._blocked = []
|
||||
|
||||
@ensure_enabled
|
||||
def _presence_received(self,
|
||||
_con: ConnectionT,
|
||||
stanza: nbxmpp.Presence,
|
||||
_properties: Any) -> None:
|
||||
if stanza.getType() not in ('unavailable', 'error'):
|
||||
return
|
||||
|
||||
full_jid = stanza.getFrom()
|
||||
if full_jid is None or self._con.get_own_jid().bare_match(full_jid):
|
||||
# Presence from ourself
|
||||
return
|
||||
|
||||
contact = app.contacts.get_gc_contact(
|
||||
self._account, full_jid.bare, full_jid.resource)
|
||||
if contact is None:
|
||||
contact = app.contacts.get_contact_from_full_jid(
|
||||
self._account, str(full_jid))
|
||||
if contact is None:
|
||||
return
|
||||
|
||||
if contact.chatstate is None:
|
||||
return
|
||||
|
||||
if contact.is_gc_contact:
|
||||
jid = contact.get_full_jid()
|
||||
else:
|
||||
jid = contact.jid
|
||||
|
||||
contact.chatstate = None
|
||||
self._chatstates.pop(jid, None)
|
||||
self._last_mouse_activity.pop(jid, None)
|
||||
self._last_keyboard_activity.pop(jid, None)
|
||||
|
||||
self._log.info('Reset chatstate for %s', jid)
|
||||
|
||||
app.nec.push_outgoing_event(
|
||||
NetworkEvent('chatstate-received',
|
||||
account=self._account,
|
||||
contact=contact))
|
||||
|
||||
def _process_chatstate(self, _con, _stanza, properties):
|
||||
if not properties.has_chatstate:
|
||||
return
|
||||
|
||||
if (properties.is_self_message or
|
||||
properties.type.is_groupchat or
|
||||
properties.is_mam_message or
|
||||
properties.is_carbon_message and properties.carbon.is_sent):
|
||||
return
|
||||
|
||||
if properties.is_muc_pm:
|
||||
contact = app.contacts.get_gc_contact(
|
||||
self._account,
|
||||
properties.jid.bare,
|
||||
properties.jid.resource)
|
||||
else:
|
||||
contact = app.contacts.get_contact_from_full_jid(
|
||||
self._account, str(properties.jid))
|
||||
if contact is None:
|
||||
return
|
||||
|
||||
contact.chatstate = properties.chatstate
|
||||
self._log.info('Recv: %-10s - %s', properties.chatstate, properties.jid)
|
||||
app.nec.push_outgoing_event(
|
||||
NetworkEvent('chatstate-received',
|
||||
account=self._account,
|
||||
contact=contact))
|
||||
|
||||
@ensure_enabled
|
||||
def _check_last_interaction(self) -> GLib.SOURCE_CONTINUE:
|
||||
now = time.time()
|
||||
for jid in list(self._last_mouse_activity.keys()):
|
||||
time_ = self._last_mouse_activity[jid]
|
||||
current_state = self._chatstates.get(jid)
|
||||
if current_state is None:
|
||||
self._last_mouse_activity.pop(jid, None)
|
||||
self._last_keyboard_activity.pop(jid, None)
|
||||
continue
|
||||
|
||||
if current_state in (State.GONE, State.INACTIVE):
|
||||
continue
|
||||
|
||||
new_chatstate = None
|
||||
if now - time_ > INACTIVE_AFTER:
|
||||
new_chatstate = State.INACTIVE
|
||||
|
||||
elif current_state == State.COMPOSING:
|
||||
key_time = self._last_keyboard_activity[jid]
|
||||
if now - key_time > PAUSED_AFTER:
|
||||
new_chatstate = State.PAUSED
|
||||
|
||||
if new_chatstate is not None:
|
||||
if self._chatstates.get(jid) != new_chatstate:
|
||||
contact = app.contacts.get_contact(self._account, jid)
|
||||
if contact is None:
|
||||
room, nick = app.get_room_and_nick_from_fjid(jid)
|
||||
contact = app.contacts.get_gc_contact(
|
||||
self._account, room, nick)
|
||||
if contact is not None:
|
||||
contact = contact.as_contact()
|
||||
else:
|
||||
# Contact not found, maybe we left the group chat
|
||||
# or the contact was removed from the roster
|
||||
self._log.info(
|
||||
'Contact %s not found, reset chatstate', jid)
|
||||
self._chatstates.pop(jid, None)
|
||||
self._last_mouse_activity.pop(jid, None)
|
||||
self._last_keyboard_activity.pop(jid, None)
|
||||
continue
|
||||
self.set_chatstate(contact, new_chatstate)
|
||||
|
||||
return GLib.SOURCE_CONTINUE
|
||||
|
||||
@ensure_enabled
|
||||
def set_active(self, contact: ContactT) -> None:
|
||||
if contact.settings.get('send_chatstate') == 'disabled':
|
||||
return
|
||||
self._last_mouse_activity[contact.jid] = time.time()
|
||||
self._chatstates[contact.jid] = State.ACTIVE
|
||||
|
||||
def get_active_chatstate(self, contact: ContactT) -> Optional[str]:
|
||||
# determines if we add 'active' on outgoing messages
|
||||
if contact.settings.get('send_chatstate') == 'disabled':
|
||||
return None
|
||||
|
||||
if not contact.is_groupchat:
|
||||
# Don’t send chatstates to ourself
|
||||
if self._con.get_own_jid().bare_match(contact.jid):
|
||||
return None
|
||||
|
||||
if not contact.supports(Namespace.CHATSTATES):
|
||||
return None
|
||||
|
||||
self.set_active(contact)
|
||||
return 'active'
|
||||
|
||||
@ensure_enabled
|
||||
def block_chatstates(self, contact: ContactT, block: bool) -> None:
|
||||
# Block sending chatstates to a contact
|
||||
# Used for example if we cycle through the MUC nick list, which
|
||||
# produces a lot of buffer 'changed' signals from the input textview.
|
||||
# This would lead to sending ACTIVE -> COMPOSING -> ACTIVE ...
|
||||
if block:
|
||||
self._blocked.append(contact.jid)
|
||||
else:
|
||||
self._blocked.remove(contact.jid)
|
||||
|
||||
@ensure_enabled
|
||||
def set_chatstate_delayed(self, contact: ContactT, state: State) -> None:
|
||||
# Used when we go from Composing -> Active after deleting all text
|
||||
# from the Textview. We delay the Active state because maybe the
|
||||
# User starts writing again.
|
||||
self.remove_delay_timeout(contact)
|
||||
self._delay_timeout_ids[contact.jid] = GLib.timeout_add_seconds(
|
||||
2, self.set_chatstate, contact, state)
|
||||
|
||||
@ensure_enabled
|
||||
def set_chatstate(self, contact: ContactT, state: State) -> None:
|
||||
# Don’t send chatstates to ourself
|
||||
if self._con.get_own_jid().bare_match(contact.jid):
|
||||
return
|
||||
|
||||
if contact.jid in self._blocked:
|
||||
return
|
||||
|
||||
self.remove_delay_timeout(contact)
|
||||
current_state = self._chatstates.get(contact.jid)
|
||||
setting = contact.settings.get('send_chatstate')
|
||||
if setting == 'disabled':
|
||||
# Send a last 'active' state after user disabled chatstates
|
||||
if current_state is not None:
|
||||
self._log.info('Disabled for %s', contact.jid)
|
||||
self._log.info('Send last state: %-10s - %s',
|
||||
State.ACTIVE, contact.jid)
|
||||
|
||||
self._send_chatstate(contact, str(State.ACTIVE))
|
||||
|
||||
self._chatstates.pop(contact.jid, None)
|
||||
self._last_mouse_activity.pop(contact.jid, None)
|
||||
self._last_keyboard_activity.pop(contact.jid, None)
|
||||
return
|
||||
|
||||
if not contact.is_groupchat:
|
||||
# Don’t leak presence to contacts
|
||||
# which are not allowed to see our status
|
||||
if not contact.is_pm_contact:
|
||||
if contact and contact.sub in ('to', 'none'):
|
||||
self._log.info('Contact not subscribed: %s', contact.jid)
|
||||
return
|
||||
|
||||
if contact.show == 'offline':
|
||||
self._log.info('Contact offline: %s', contact.jid)
|
||||
return
|
||||
|
||||
if not contact.supports(Namespace.CHATSTATES):
|
||||
self._log.info('Chatstates not supported: %s', contact.jid)
|
||||
return
|
||||
|
||||
if state in (State.ACTIVE, State.COMPOSING):
|
||||
self._last_mouse_activity[contact.jid] = time.time()
|
||||
|
||||
if setting == 'composing_only':
|
||||
if state in (State.INACTIVE, State.GONE):
|
||||
state = State.ACTIVE
|
||||
|
||||
if current_state == state:
|
||||
return
|
||||
|
||||
self._log.info('Send: %-10s - %s', state, contact.jid)
|
||||
|
||||
self._send_chatstate(contact, str(state))
|
||||
|
||||
self._chatstates[contact.jid] = state
|
||||
|
||||
def _send_chatstate(self, contact, chatstate):
|
||||
type_ = 'groupchat' if contact.is_groupchat else 'chat'
|
||||
message = OutgoingMessage(account=self._account,
|
||||
contact=contact,
|
||||
message=None,
|
||||
type_=type_,
|
||||
chatstate=chatstate,
|
||||
play_sound=False)
|
||||
|
||||
self._con.send_message(message)
|
||||
|
||||
@ensure_enabled
|
||||
def set_mouse_activity(self, contact: ContactT, was_paused: bool) -> None:
|
||||
if contact.settings.get('send_chatstate') == 'disabled':
|
||||
return
|
||||
self._last_mouse_activity[contact.jid] = time.time()
|
||||
if self._chatstates.get(contact.jid) == State.INACTIVE:
|
||||
if was_paused:
|
||||
self.set_chatstate(contact, State.PAUSED)
|
||||
else:
|
||||
self.set_chatstate(contact, State.ACTIVE)
|
||||
|
||||
@ensure_enabled
|
||||
def set_keyboard_activity(self, contact: ContactT) -> None:
|
||||
self._last_keyboard_activity[contact.jid] = time.time()
|
||||
|
||||
def remove_delay_timeout(self, contact):
|
||||
timeout = self._delay_timeout_ids.get(contact.jid)
|
||||
if timeout is not None:
|
||||
GLib.source_remove(timeout)
|
||||
del self._delay_timeout_ids[contact.jid]
|
||||
|
||||
def remove_all_delay_timeouts(self):
|
||||
for timeout in self._delay_timeout_ids.values():
|
||||
GLib.source_remove(timeout)
|
||||
self._delay_timeout_ids = {}
|
||||
|
||||
def cleanup(self):
|
||||
self.remove_all_delay_timeouts()
|
||||
if self._timeout_id is not None:
|
||||
GLib.source_remove(self._timeout_id)
|
||||
|
||||
|
||||
def get_instance(*args: Any, **kwargs: Any) -> Tuple[Chatstate, str]:
|
||||
return Chatstate(*args, **kwargs), 'Chatstate'
|
56
gajim/common/modules/delimiter.py
Normal file
56
gajim/common/modules/delimiter.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
# 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-0083: Nested Roster Groups
|
||||
|
||||
|
||||
from nbxmpp.errors import is_error
|
||||
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.modules.util import as_task
|
||||
|
||||
|
||||
class Delimiter(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'Delimiter'
|
||||
_nbxmpp_methods = [
|
||||
'request_delimiter',
|
||||
'set_delimiter'
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
self.available = False
|
||||
self.delimiter = '::'
|
||||
|
||||
@as_task
|
||||
def get_roster_delimiter(self):
|
||||
_task = yield
|
||||
|
||||
delimiter = yield self.request_delimiter()
|
||||
if is_error(delimiter) or delimiter is None:
|
||||
result = yield self.set_delimiter(self.delimiter)
|
||||
if is_error(result):
|
||||
self._con.connect_machine()
|
||||
return
|
||||
|
||||
delimiter = self.delimiter
|
||||
|
||||
self.delimiter = delimiter
|
||||
self.available = True
|
||||
self._con.connect_machine()
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return Delimiter(*args, **kwargs), 'Delimiter'
|
265
gajim/common/modules/discovery.py
Normal file
265
gajim/common/modules/discovery.py
Normal file
|
@ -0,0 +1,265 @@
|
|||
# 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 nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
from nbxmpp.errors import StanzaError
|
||||
from nbxmpp.errors import is_error
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkIncomingEvent
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.modules.util import as_task
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class Discovery(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'Discovery'
|
||||
_nbxmpp_methods = [
|
||||
'disco_info',
|
||||
'disco_items',
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='iq',
|
||||
callback=self._answer_disco_info,
|
||||
typ='get',
|
||||
ns=Namespace.DISCO_INFO),
|
||||
StanzaHandler(name='iq',
|
||||
callback=self._answer_disco_items,
|
||||
typ='get',
|
||||
ns=Namespace.DISCO_ITEMS),
|
||||
]
|
||||
|
||||
self._account_info = None
|
||||
self._server_info = None
|
||||
|
||||
@property
|
||||
def account_info(self):
|
||||
return self._account_info
|
||||
|
||||
@property
|
||||
def server_info(self):
|
||||
return self._server_info
|
||||
|
||||
def discover_server_items(self):
|
||||
server = self._con.get_own_jid().domain
|
||||
self.disco_items(server, callback=self._server_items_received)
|
||||
|
||||
def _server_items_received(self, task):
|
||||
try:
|
||||
result = task.finish()
|
||||
except StanzaError as error:
|
||||
self._log.warning('Server disco failed')
|
||||
self._log.error(error)
|
||||
return
|
||||
|
||||
self._log.info('Server items received')
|
||||
self._log.debug(result)
|
||||
for item in result.items:
|
||||
if item.node is not None:
|
||||
# Only disco components
|
||||
continue
|
||||
self.disco_info(item.jid, callback=self._server_items_info_received)
|
||||
|
||||
def _server_items_info_received(self, task):
|
||||
try:
|
||||
result = task.finish()
|
||||
except StanzaError as error:
|
||||
self._log.warning('Server item disco info failed')
|
||||
self._log.warning(error)
|
||||
return
|
||||
|
||||
self._log.info('Server item info received: %s', result.jid)
|
||||
self._parse_transports(result)
|
||||
try:
|
||||
self._con.get_module('MUC').pass_disco(result)
|
||||
self._con.get_module('HTTPUpload').pass_disco(result)
|
||||
self._con.get_module('Bytestream').pass_disco(result)
|
||||
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().bare
|
||||
self.disco_info(own_jid, callback=self._account_info_received)
|
||||
|
||||
def _account_info_received(self, task):
|
||||
try:
|
||||
result = task.finish()
|
||||
except StanzaError as error:
|
||||
self._log.warning('Account disco info failed')
|
||||
self._log.warning(error)
|
||||
return
|
||||
|
||||
self._log.info('Account info received: %s', result.jid)
|
||||
|
||||
self._account_info = result
|
||||
|
||||
self._con.get_module('MAM').pass_disco(result)
|
||||
self._con.get_module('PEP').pass_disco(result)
|
||||
self._con.get_module('PubSub').pass_disco(result)
|
||||
self._con.get_module('Bookmarks').pass_disco(result)
|
||||
self._con.get_module('VCardAvatars').pass_disco(result)
|
||||
|
||||
self._con.get_module('Caps').update_caps()
|
||||
|
||||
def discover_server_info(self):
|
||||
# Calling this method starts the connect_maschine()
|
||||
server = self._con.get_own_jid().domain
|
||||
self.disco_info(server, callback=self._server_info_received)
|
||||
|
||||
def _server_info_received(self, task):
|
||||
try:
|
||||
result = task.finish()
|
||||
except StanzaError as error:
|
||||
self._log.error('Server disco info failed')
|
||||
self._log.error(error)
|
||||
return
|
||||
|
||||
self._log.info('Server info received: %s', result.jid)
|
||||
|
||||
self._server_info = result
|
||||
|
||||
self._con.get_module('SecLabels').pass_disco(result)
|
||||
self._con.get_module('Blocking').pass_disco(result)
|
||||
self._con.get_module('VCardTemp').pass_disco(result)
|
||||
self._con.get_module('Carbons').pass_disco(result)
|
||||
self._con.get_module('HTTPUpload').pass_disco(result)
|
||||
self._con.get_module('Register').pass_disco(result)
|
||||
|
||||
self._con.connect_machine(restart=True)
|
||||
|
||||
def _parse_transports(self, info):
|
||||
for identity in info.identities:
|
||||
if identity.category not in ('gateway', 'headline'):
|
||||
continue
|
||||
|
||||
self._log.info('Found transport: %s %s %s',
|
||||
info.jid, identity.category, identity.type)
|
||||
|
||||
jid = str(info.jid)
|
||||
if jid not in app.transport_type:
|
||||
app.transport_type[jid] = identity.type
|
||||
|
||||
if identity.type in self._con.available_transports:
|
||||
self._con.available_transports[identity.type].append(jid)
|
||||
else:
|
||||
self._con.available_transports[identity.type] = [jid]
|
||||
|
||||
def _answer_disco_items(self, _con, stanza, _properties):
|
||||
from_ = stanza.getFrom()
|
||||
self._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 == Namespace.COMMANDS:
|
||||
self._con.get_module('AdHocCommands').command_list_query(stanza)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _answer_disco_info(self, _con, stanza, _properties):
|
||||
from_ = stanza.getFrom()
|
||||
self._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
|
||||
|
||||
@as_task
|
||||
def disco_muc(self,
|
||||
jid,
|
||||
request_vcard=False,
|
||||
allow_redirect=False):
|
||||
|
||||
_task = yield
|
||||
|
||||
self._log.info('Request MUC info for %s', jid)
|
||||
|
||||
result = yield self._nbxmpp('MUC').request_info(
|
||||
jid,
|
||||
request_vcard=request_vcard,
|
||||
allow_redirect=allow_redirect)
|
||||
|
||||
if is_error(result):
|
||||
raise result
|
||||
|
||||
if result.redirected:
|
||||
self._log.info('MUC info received after redirect: %s -> %s',
|
||||
jid, result.info.jid)
|
||||
else:
|
||||
self._log.info('MUC info received: %s', result.info.jid)
|
||||
|
||||
app.storage.cache.set_last_disco_info(result.info.jid, result.info)
|
||||
|
||||
if result.vcard is not None:
|
||||
avatar, avatar_sha = result.vcard.get_avatar()
|
||||
if avatar is not None:
|
||||
if not app.interface.avatar_exists(avatar_sha):
|
||||
app.interface.save_avatar(avatar)
|
||||
|
||||
app.storage.cache.set_muc_avatar_sha(result.info.jid,
|
||||
avatar_sha)
|
||||
app.interface.avatar_storage.invalidate_cache(result.info.jid)
|
||||
|
||||
self._con.get_module('VCardAvatars').muc_disco_info_update(result.info)
|
||||
app.nec.push_incoming_event(NetworkEvent(
|
||||
'muc-disco-update',
|
||||
account=self._account,
|
||||
room_jid=result.info.jid))
|
||||
|
||||
yield result
|
||||
|
||||
@as_task
|
||||
def disco_contact(self, contact):
|
||||
_task = yield
|
||||
|
||||
fjid = contact.get_full_jid()
|
||||
|
||||
result = yield self.disco_info(fjid)
|
||||
if is_error(result):
|
||||
raise result
|
||||
|
||||
self._log.info('Disco Info received: %s', fjid)
|
||||
|
||||
app.storage.cache.set_last_disco_info(result.jid,
|
||||
result,
|
||||
cache_only=True)
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('caps-update',
|
||||
account=self._account,
|
||||
fjid=fjid,
|
||||
jid=contact.jid))
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return Discovery(*args, **kwargs), 'Discovery'
|
116
gajim/common/modules/entity_time.py
Normal file
116
gajim/common/modules/entity_time.py
Normal file
|
@ -0,0 +1,116 @@
|
|||
# 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-0202: Entity Time
|
||||
|
||||
import time
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
from nbxmpp.modules.date_and_time import parse_datetime
|
||||
from nbxmpp.modules.date_and_time import create_tzinfo
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class EntityTime(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='iq',
|
||||
callback=self._answer_request,
|
||||
typ='get',
|
||||
ns=Namespace.TIME_REVISED),
|
||||
]
|
||||
|
||||
def request_entity_time(self, jid, resource):
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
|
||||
if resource:
|
||||
jid += '/' + resource
|
||||
iq = nbxmpp.Iq(to=jid, typ='get')
|
||||
iq.addChild('time', namespace=Namespace.TIME_REVISED)
|
||||
|
||||
self._log.info('Requested: %s', jid)
|
||||
|
||||
self._con.connection.SendAndCallForResponse(iq, self._result_received)
|
||||
|
||||
def _result_received(self, _nbxmpp_client, stanza):
|
||||
time_info = None
|
||||
if not nbxmpp.isResultNode(stanza):
|
||||
self._log.info('Error: %s', stanza.getError())
|
||||
else:
|
||||
time_info = self._extract_info(stanza)
|
||||
|
||||
self._log.info('Received: %s %s', stanza.getFrom(), time_info)
|
||||
|
||||
app.nec.push_incoming_event(NetworkEvent('time-result-received',
|
||||
conn=self._con,
|
||||
jid=stanza.getFrom(),
|
||||
time_info=time_info))
|
||||
|
||||
def _extract_info(self, stanza):
|
||||
time_ = stanza.getTag('time')
|
||||
if not time_:
|
||||
self._log.warning('No time node: %s', stanza)
|
||||
return None
|
||||
|
||||
tzo = time_.getTag('tzo').getData()
|
||||
if not tzo:
|
||||
self._log.warning('Wrong tzo node: %s', stanza)
|
||||
return None
|
||||
|
||||
remote_tz = create_tzinfo(tz_string=tzo)
|
||||
if remote_tz is None:
|
||||
self._log.warning('Wrong tzo node: %s', stanza)
|
||||
return None
|
||||
|
||||
utc_time = time_.getTag('utc').getData()
|
||||
date_time = parse_datetime(utc_time, check_utc=True)
|
||||
if date_time is None:
|
||||
self._log.warning('Wrong timezone definition: %s %s',
|
||||
utc_time, stanza.getFrom())
|
||||
return None
|
||||
|
||||
date_time = date_time.astimezone(remote_tz)
|
||||
return date_time.strftime('%c %Z')
|
||||
|
||||
def _answer_request(self, _con, stanza, _properties):
|
||||
self._log.info('%s asked for the time', stanza.getFrom())
|
||||
if app.settings.get_account_setting(self._account, 'send_time_info'):
|
||||
iq = stanza.buildReply('result')
|
||||
time_ = iq.setTag('time', namespace=Namespace.TIME_REVISED)
|
||||
formated_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
||||
time_.setTagData('utc', formated_time)
|
||||
isdst = time.localtime().tm_isdst
|
||||
zone = -(time.timezone, time.altzone)[isdst] / 60.0
|
||||
tzo = (zone / 60, abs(zone % 60))
|
||||
time_.setTagData('tzo', '%+03d:%02d' % (tzo))
|
||||
self._log.info('Answer: %s %s', formated_time, '%+03d:%02d' % (tzo))
|
||||
else:
|
||||
iq = stanza.buildReply('error')
|
||||
err = nbxmpp.ErrorNode(nbxmpp.ERR_SERVICE_UNAVAILABLE)
|
||||
iq.addChild(node=err)
|
||||
self._log.info('Send service-unavailable')
|
||||
self._con.connection.send(iq)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return EntityTime(*args, **kwargs), 'EntityTime'
|
101
gajim/common/modules/gateway.py
Normal file
101
gajim/common/modules/gateway.py
Normal file
|
@ -0,0 +1,101 @@
|
|||
# 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-0100: Gateway Interaction
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class Gateway(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
def unsubscribe(self, agent):
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
iq = nbxmpp.Iq('set', Namespace.REGISTER, to=agent)
|
||||
iq.setQuery().setTag('remove')
|
||||
|
||||
self._con.connection.SendAndCallForResponse(
|
||||
iq, self._on_unsubscribe_result)
|
||||
self._con.get_module('Roster').del_item(agent)
|
||||
|
||||
def _on_unsubscribe_result(self, _nbxmpp_client, stanza):
|
||||
if not nbxmpp.isResultNode(stanza):
|
||||
self._log.info('Error: %s', stanza.getError())
|
||||
return
|
||||
|
||||
agent = stanza.getFrom().bare
|
||||
jid_list = []
|
||||
for jid in app.contacts.get_jid_list(self._account):
|
||||
if jid.endswith('@' + agent):
|
||||
jid_list.append(jid)
|
||||
self._log.info('Removing contact %s due to'
|
||||
' unregistered transport %s', jid, agent)
|
||||
self._con.get_module('Presence').unsubscribe(jid)
|
||||
# Transport contacts can't have 2 resources
|
||||
if jid in app.to_be_removed[self._account]:
|
||||
# This way we'll really remove it
|
||||
app.to_be_removed[self._account].remove(jid)
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('agent-removed',
|
||||
conn=self._con,
|
||||
agent=agent,
|
||||
jid_list=jid_list))
|
||||
|
||||
def request_gateway_prompt(self, jid, prompt=None):
|
||||
typ_ = 'get'
|
||||
if prompt:
|
||||
typ_ = 'set'
|
||||
iq = nbxmpp.Iq(typ=typ_, to=jid)
|
||||
query = iq.addChild(name='query', namespace=Namespace.GATEWAY)
|
||||
if prompt:
|
||||
query.setTagData('prompt', prompt)
|
||||
self._con.connection.SendAndCallForResponse(iq, self._on_prompt_result)
|
||||
|
||||
def _on_prompt_result(self, _nbxmpp_client, stanza):
|
||||
jid = str(stanza.getFrom())
|
||||
fjid = stanza.getFrom().bare
|
||||
resource = stanza.getFrom().resource
|
||||
|
||||
query = stanza.getTag('query')
|
||||
if query is not None:
|
||||
desc = query.getTagData('desc')
|
||||
prompt = query.getTagData('prompt')
|
||||
prompt_jid = query.getTagData('jid')
|
||||
else:
|
||||
desc = None
|
||||
prompt = None
|
||||
prompt_jid = None
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('gateway-prompt-received',
|
||||
conn=self._con,
|
||||
fjid=fjid,
|
||||
jid=jid,
|
||||
resource=resource,
|
||||
desc=desc,
|
||||
prompt=prompt,
|
||||
prompt_jid=prompt_jid,
|
||||
stanza=stanza))
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return Gateway(*args, **kwargs), 'Gateway'
|
78
gajim/common/modules/http_auth.py
Normal file
78
gajim/common/modules/http_auth.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
# 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-0070: Verifying HTTP Requests via XMPP
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
from nbxmpp.namespaces import Namespace
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class HTTPAuth(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='message',
|
||||
callback=self._http_auth,
|
||||
ns=Namespace.HTTP_AUTH,
|
||||
priority=45),
|
||||
StanzaHandler(name='iq',
|
||||
callback=self._http_auth,
|
||||
typ='get',
|
||||
ns=Namespace.HTTP_AUTH,
|
||||
priority=45)
|
||||
]
|
||||
|
||||
def _http_auth(self, _con, stanza, properties):
|
||||
if not properties.is_http_auth:
|
||||
return
|
||||
|
||||
self._log.info('Auth request received')
|
||||
auto_answer = app.settings.get_account_setting(self._account,
|
||||
'http_auth')
|
||||
if auto_answer in ('yes', 'no'):
|
||||
self.build_http_auth_answer(stanza, auto_answer)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('http-auth-received',
|
||||
conn=self._con,
|
||||
iq_id=properties.http_auth.id,
|
||||
method=properties.http_auth.method,
|
||||
url=properties.http_auth.url,
|
||||
msg=properties.http_auth.body,
|
||||
stanza=stanza))
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def build_http_auth_answer(self, stanza, answer):
|
||||
if answer == 'yes':
|
||||
self._log.info('Auth request approved')
|
||||
confirm = stanza.getTag('confirm')
|
||||
reply = stanza.buildReply('result')
|
||||
if stanza.getName() == 'message':
|
||||
reply.addChild(node=confirm)
|
||||
self._con.connection.send(reply)
|
||||
elif answer == 'no':
|
||||
self._log.info('Auth request denied')
|
||||
err = nbxmpp.Error(stanza, nbxmpp.protocol.ERR_NOT_AUTHORIZED)
|
||||
self._con.connection.send(err)
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return HTTPAuth(*args, **kwargs), 'HTTPAuth'
|
404
gajim/common/modules/httpupload.py
Normal file
404
gajim/common/modules/httpupload.py
Normal file
|
@ -0,0 +1,404 @@
|
|||
# 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-0363: HTTP File Upload
|
||||
|
||||
|
||||
import os
|
||||
import io
|
||||
from urllib.parse import urlparse
|
||||
import mimetypes
|
||||
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.errors import StanzaError
|
||||
from nbxmpp.errors import MalformedStanzaError
|
||||
from nbxmpp.errors import HTTPUploadStanzaError
|
||||
from nbxmpp.util import convert_tls_error_flags
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Soup
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.i18n import _
|
||||
from gajim.common.helpers import get_tls_error_phrase
|
||||
from gajim.common.helpers import get_user_proxy
|
||||
from gajim.common.const import FTState
|
||||
from gajim.common.filetransfer import FileTransfer
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.exceptions import FileError
|
||||
|
||||
|
||||
class HTTPUpload(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'HTTPUpload'
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.available = False
|
||||
self.component = None
|
||||
self.httpupload_namespace = None
|
||||
self.max_file_size = None # maximum file size in bytes
|
||||
|
||||
self._proxy_resolver = None
|
||||
self._queued_messages = {}
|
||||
self._session = Soup.Session()
|
||||
self._session.props.ssl_strict = False
|
||||
self._session.props.user_agent = 'Gajim %s' % app.version
|
||||
|
||||
def _set_proxy_if_available(self):
|
||||
proxy = get_user_proxy(self._account)
|
||||
if proxy is None:
|
||||
self._proxy_resolver = None
|
||||
self._session.props.proxy_resolver = None
|
||||
else:
|
||||
self._proxy_resolver = proxy.get_resolver()
|
||||
self._session.props.proxy_resolver = self._proxy_resolver
|
||||
|
||||
def pass_disco(self, info):
|
||||
if not info.has_httpupload:
|
||||
return
|
||||
|
||||
self.available = True
|
||||
self.httpupload_namespace = Namespace.HTTPUPLOAD_0
|
||||
self.component = info.jid
|
||||
self.max_file_size = info.httpupload_max_file_size
|
||||
|
||||
self._log.info('Discovered component: %s', info.jid)
|
||||
|
||||
if self.max_file_size is None:
|
||||
self._log.warning('Component does not provide maximum file size')
|
||||
else:
|
||||
size = GLib.format_size_full(self.max_file_size,
|
||||
GLib.FormatSizeFlags.IEC_UNITS)
|
||||
self._log.info('Component has a maximum file size of: %s', size)
|
||||
|
||||
for ctrl in app.interface.msg_win_mgr.get_controls(acct=self._account):
|
||||
ctrl.update_actions()
|
||||
|
||||
def make_transfer(self, path, encryption, contact, groupchat=False):
|
||||
if not path or not os.path.exists(path):
|
||||
raise FileError(_('Could not access file'))
|
||||
|
||||
invalid_file = False
|
||||
stat = os.stat(path)
|
||||
|
||||
if os.path.isfile(path):
|
||||
if stat[6] == 0:
|
||||
invalid_file = True
|
||||
msg = _('File is empty')
|
||||
else:
|
||||
invalid_file = True
|
||||
msg = _('File does not exist')
|
||||
|
||||
if self.max_file_size is not None and \
|
||||
stat.st_size > self.max_file_size:
|
||||
invalid_file = True
|
||||
size = GLib.format_size_full(self.max_file_size,
|
||||
GLib.FormatSizeFlags.IEC_UNITS)
|
||||
msg = _('File is too large, '
|
||||
'maximum allowed file size is: %s') % size
|
||||
|
||||
if invalid_file:
|
||||
raise FileError(msg)
|
||||
|
||||
mime = mimetypes.MimeTypes().guess_type(path)[0]
|
||||
if not mime:
|
||||
mime = 'application/octet-stream' # fallback mime type
|
||||
self._log.info("Detected MIME type of file: %s", mime)
|
||||
|
||||
return HTTPFileTransfer(self._account,
|
||||
path,
|
||||
contact,
|
||||
mime,
|
||||
encryption,
|
||||
groupchat)
|
||||
|
||||
def cancel_transfer(self, transfer):
|
||||
transfer.set_cancelled()
|
||||
message = self._queued_messages.get(id(transfer))
|
||||
if message is None:
|
||||
return
|
||||
|
||||
self._session.cancel_message(message, Soup.Status.CANCELLED)
|
||||
|
||||
def start_transfer(self, transfer):
|
||||
if transfer.encryption is not None and not transfer.is_encrypted:
|
||||
transfer.set_encrypting()
|
||||
plugin = app.plugin_manager.encryption_plugins[transfer.encryption]
|
||||
if hasattr(plugin, 'encrypt_file'):
|
||||
plugin.encrypt_file(transfer,
|
||||
self._account,
|
||||
self.start_transfer)
|
||||
else:
|
||||
transfer.set_error('encryption-not-available')
|
||||
|
||||
return
|
||||
|
||||
transfer.set_preparing()
|
||||
self._log.info('Sending request for slot')
|
||||
self._nbxmpp('HTTPUpload').request_slot(
|
||||
jid=self.component,
|
||||
filename=transfer.filename,
|
||||
size=transfer.size,
|
||||
content_type=transfer.mime,
|
||||
callback=self._received_slot,
|
||||
user_data=transfer)
|
||||
|
||||
def _received_slot(self, task):
|
||||
transfer = task.get_user_data()
|
||||
|
||||
try:
|
||||
result = task.finish()
|
||||
except (StanzaError,
|
||||
HTTPUploadStanzaError,
|
||||
MalformedStanzaError) as error:
|
||||
|
||||
if error.app_condition == 'file-too-large':
|
||||
size_text = GLib.format_size_full(
|
||||
error.get_max_file_size(),
|
||||
GLib.FormatSizeFlags.IEC_UNITS)
|
||||
|
||||
error_text = _('File is too large, '
|
||||
'maximum allowed file size is: %s' % size_text)
|
||||
transfer.set_error('file-too-large', error_text)
|
||||
|
||||
else:
|
||||
transfer.set_error('misc', str(error))
|
||||
|
||||
return
|
||||
|
||||
transfer.process_result(result)
|
||||
|
||||
if (urlparse(transfer.put_uri).scheme != 'https' or
|
||||
urlparse(transfer.get_uri).scheme != 'https'):
|
||||
transfer.set_error('unsecure')
|
||||
return
|
||||
|
||||
self._log.info('Uploading file to %s', transfer.put_uri)
|
||||
self._log.info('Please download from %s', transfer.get_uri)
|
||||
|
||||
self._upload_file(transfer)
|
||||
|
||||
def _upload_file(self, transfer):
|
||||
transfer.set_started()
|
||||
|
||||
message = Soup.Message.new('PUT', transfer.put_uri)
|
||||
message.connect('starting', self._check_certificate, transfer)
|
||||
|
||||
# Set CAN_REBUILD so chunks get discarded after they have been
|
||||
# written to the network
|
||||
message.set_flags(Soup.MessageFlags.CAN_REBUILD |
|
||||
Soup.MessageFlags.NO_REDIRECT)
|
||||
|
||||
message.props.request_body.set_accumulate(False)
|
||||
|
||||
message.props.request_headers.set_content_type(transfer.mime, None)
|
||||
message.props.request_headers.set_content_length(transfer.size)
|
||||
for name, value in transfer.headers.items():
|
||||
message.props.request_headers.append(name, value)
|
||||
|
||||
message.connect('wrote-headers', self._on_wrote_headers, transfer)
|
||||
message.connect('wrote-chunk', self._on_wrote_chunk, transfer)
|
||||
|
||||
self._queued_messages[id(transfer)] = message
|
||||
self._set_proxy_if_available()
|
||||
self._session.queue_message(message, self._on_finish, transfer)
|
||||
|
||||
def _check_certificate(self, message, transfer):
|
||||
https_used, tls_certificate, tls_errors = message.get_https_status()
|
||||
if not https_used:
|
||||
self._log.warning('HTTPS was not used for upload')
|
||||
transfer.set_error('unsecure')
|
||||
self._session.cancel_message(message, Soup.Status.CANCELLED)
|
||||
return
|
||||
|
||||
tls_errors = convert_tls_error_flags(tls_errors)
|
||||
if app.cert_store.verify(tls_certificate, tls_errors):
|
||||
return
|
||||
|
||||
for error in tls_errors:
|
||||
phrase = get_tls_error_phrase(error)
|
||||
self._log.warning('TLS verification failed: %s', phrase)
|
||||
|
||||
transfer.set_error('tls-verification-failed', phrase)
|
||||
self._session.cancel_message(message, Soup.Status.CANCELLED)
|
||||
|
||||
def _on_finish(self, _session, message, transfer):
|
||||
self._queued_messages.pop(id(transfer), None)
|
||||
|
||||
if message.props.status_code == Soup.Status.CANCELLED:
|
||||
self._log.info('Upload cancelled')
|
||||
return
|
||||
|
||||
if message.props.status_code in (Soup.Status.OK, Soup.Status.CREATED):
|
||||
self._log.info('Upload completed successfully')
|
||||
transfer.set_finished()
|
||||
|
||||
|
||||
else:
|
||||
phrase = Soup.Status.get_phrase(message.props.status_code)
|
||||
self._log.error('Got unexpected http upload response code: %s',
|
||||
phrase)
|
||||
transfer.set_error('http-response', phrase)
|
||||
|
||||
def _on_wrote_chunk(self, message, transfer):
|
||||
transfer.update_progress()
|
||||
if transfer.is_complete:
|
||||
message.props.request_body.complete()
|
||||
return
|
||||
|
||||
bytes_ = transfer.get_chunk()
|
||||
self._session.pause_message(message)
|
||||
GLib.idle_add(self._append, message, bytes_)
|
||||
|
||||
def _append(self, message, bytes_):
|
||||
if message.props.status_code == Soup.Status.CANCELLED:
|
||||
return
|
||||
self._session.unpause_message(message)
|
||||
message.props.request_body.append(bytes_)
|
||||
|
||||
@staticmethod
|
||||
def _on_wrote_headers(message, transfer):
|
||||
message.props.request_body.append(transfer.get_chunk())
|
||||
|
||||
|
||||
class HTTPFileTransfer(FileTransfer):
|
||||
|
||||
_state_descriptions = {
|
||||
FTState.ENCRYPTING: _('Encrypting file…'),
|
||||
FTState.PREPARING: _('Requesting HTTP File Upload Slot…'),
|
||||
FTState.STARTED: _('Uploading via HTTP File Upload…'),
|
||||
}
|
||||
|
||||
_errors = {
|
||||
'unsecure': _('The server returned an insecure transport (HTTP).'),
|
||||
'encryption-not-available': _('There is no encryption method available '
|
||||
'for the chosen encryption.')
|
||||
}
|
||||
|
||||
def __init__(self,
|
||||
account,
|
||||
path,
|
||||
contact,
|
||||
mime,
|
||||
encryption,
|
||||
groupchat):
|
||||
|
||||
FileTransfer.__init__(self, account)
|
||||
|
||||
self._path = path
|
||||
self._encryption = encryption
|
||||
self._groupchat = groupchat
|
||||
self._contact = contact
|
||||
self._mime = mime
|
||||
|
||||
self.size = os.stat(path).st_size
|
||||
self.put_uri = None
|
||||
self.get_uri = None
|
||||
self._uri_transform_func = None
|
||||
|
||||
self._stream = None
|
||||
self._data = None
|
||||
self._headers = {}
|
||||
|
||||
self._is_encrypted = False
|
||||
|
||||
@property
|
||||
def mime(self):
|
||||
return self._mime
|
||||
|
||||
@property
|
||||
def contact(self):
|
||||
return self._contact
|
||||
|
||||
@property
|
||||
def is_groupchat(self):
|
||||
return self._groupchat
|
||||
|
||||
@property
|
||||
def encryption(self):
|
||||
return self._encryption
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
return self._headers
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def is_encrypted(self):
|
||||
return self._is_encrypted
|
||||
|
||||
def get_transformed_uri(self):
|
||||
if self._uri_transform_func is not None:
|
||||
return self._uri_transform_func(self.get_uri)
|
||||
return self.get_uri
|
||||
|
||||
def set_uri_transform_func(self, func):
|
||||
self._uri_transform_func = func
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
return os.path.basename(self._path)
|
||||
|
||||
def set_error(self, domain, text=''):
|
||||
if not text:
|
||||
text = self._errors[domain]
|
||||
|
||||
self._close()
|
||||
super().set_error(domain, text)
|
||||
|
||||
def set_finished(self):
|
||||
self._close()
|
||||
super().set_finished()
|
||||
|
||||
def set_encrypted_data(self, data):
|
||||
self._data = data
|
||||
self._is_encrypted = True
|
||||
|
||||
def _close(self):
|
||||
if self._stream is not None:
|
||||
self._stream.close()
|
||||
|
||||
def get_chunk(self):
|
||||
if self._stream is None:
|
||||
if self._encryption is None:
|
||||
self._stream = open(self._path, 'rb')
|
||||
else:
|
||||
self._stream = io.BytesIO(self._data)
|
||||
|
||||
data = self._stream.read(16384)
|
||||
if not data:
|
||||
self._close()
|
||||
return None
|
||||
self._seen += len(data)
|
||||
if self.is_complete:
|
||||
self._close()
|
||||
return data
|
||||
|
||||
def get_data(self):
|
||||
with open(self._path, 'rb') as file:
|
||||
data = file.read()
|
||||
return data
|
||||
|
||||
def process_result(self, result):
|
||||
self.put_uri = result.put_uri
|
||||
self.get_uri = result.get_uri
|
||||
self._headers = result.headers
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return HTTPUpload(*args, **kwargs), 'HTTPUpload'
|
239
gajim/common/modules/ibb.py
Normal file
239
gajim/common/modules/ibb.py
Normal file
|
@ -0,0 +1,239 @@
|
|||
# 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-0047: In-Band Bytestreams
|
||||
|
||||
import time
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.protocol import NodeProcessed
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
from nbxmpp.errors import StanzaError
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.helpers import to_user_string
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.file_props import FilesProp
|
||||
|
||||
|
||||
class IBB(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'IBB'
|
||||
_nbxmpp_methods = [
|
||||
'send_open',
|
||||
'send_close',
|
||||
'send_data',
|
||||
'send_reply',
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='iq',
|
||||
callback=self._ibb_received,
|
||||
ns=Namespace.IBB),
|
||||
]
|
||||
|
||||
def _ibb_received(self, _con, stanza, properties):
|
||||
if not properties.is_ibb:
|
||||
return
|
||||
|
||||
if properties.ibb.type == 'data':
|
||||
self._log.info('Data received, sid: %s, seq: %s',
|
||||
properties.ibb.sid, properties.ibb.seq)
|
||||
file_props = FilesProp.getFilePropByTransportSid(self._account,
|
||||
properties.ibb.sid)
|
||||
if not file_props:
|
||||
self.send_reply(stanza, nbxmpp.ERR_ITEM_NOT_FOUND)
|
||||
raise NodeProcessed
|
||||
|
||||
if file_props.connected:
|
||||
self._on_data_received(stanza, file_props, properties)
|
||||
self.send_reply(stanza)
|
||||
|
||||
elif properties.ibb.type == 'open':
|
||||
self._log.info('Open received, sid: %s, blocksize: %s',
|
||||
properties.ibb.sid, properties.ibb.block_size)
|
||||
|
||||
file_props = FilesProp.getFilePropByTransportSid(self._account,
|
||||
properties.ibb.sid)
|
||||
if not file_props:
|
||||
self.send_reply(stanza, nbxmpp.ERR_ITEM_NOT_FOUND)
|
||||
raise NodeProcessed
|
||||
|
||||
file_props.block_size = properties.ibb.block_size
|
||||
file_props.direction = '<'
|
||||
file_props.seq = 0
|
||||
file_props.received_len = 0
|
||||
file_props.last_time = time.time()
|
||||
file_props.error = 0
|
||||
file_props.paused = False
|
||||
file_props.connected = True
|
||||
file_props.completed = False
|
||||
file_props.disconnect_cb = None
|
||||
file_props.continue_cb = None
|
||||
file_props.syn_id = stanza.getID()
|
||||
file_props.fp = open(file_props.file_name, 'wb')
|
||||
self.send_reply(stanza)
|
||||
|
||||
elif properties.ibb.type == 'close':
|
||||
self._log.info('Close received, sid: %s', properties.ibb.sid)
|
||||
file_props = FilesProp.getFilePropByTransportSid(self._account,
|
||||
properties.ibb.sid)
|
||||
if not file_props:
|
||||
self.send_reply(stanza, nbxmpp.ERR_ITEM_NOT_FOUND)
|
||||
raise NodeProcessed
|
||||
|
||||
self.send_reply(stanza)
|
||||
file_props.fp.close()
|
||||
file_props.completed = file_props.received_len >= file_props.size
|
||||
if not file_props.completed:
|
||||
file_props.error = -1
|
||||
app.socks5queue.complete_transfer_cb(self._account, file_props)
|
||||
|
||||
raise NodeProcessed
|
||||
|
||||
def _on_data_received(self, stanza, file_props, properties):
|
||||
ibb = properties.ibb
|
||||
if ibb.seq != file_props.seq:
|
||||
self.send_reply(stanza, nbxmpp.ERR_UNEXPECTED_REQUEST)
|
||||
self.send_close(file_props)
|
||||
raise NodeProcessed
|
||||
|
||||
self._log.debug('Data received: sid: %s, %s+%s bytes',
|
||||
ibb.sid, file_props.fp.tell(), len(ibb.data))
|
||||
|
||||
file_props.seq += 1
|
||||
file_props.started = True
|
||||
file_props.fp.write(ibb.data)
|
||||
current_time = time.time()
|
||||
file_props.elapsed_time += current_time - file_props.last_time
|
||||
file_props.last_time = current_time
|
||||
file_props.received_len += len(ibb.data)
|
||||
app.socks5queue.progress_transfer_cb(self._account, file_props)
|
||||
if file_props.received_len >= file_props.size:
|
||||
file_props.completed = True
|
||||
|
||||
def send_open(self, to, sid, fp):
|
||||
self._log.info('Send open to %s, sid: %s', to, sid)
|
||||
file_props = FilesProp.getFilePropBySid(sid)
|
||||
file_props.direction = '>'
|
||||
file_props.block_size = 4096
|
||||
file_props.fp = fp
|
||||
file_props.seq = -1
|
||||
file_props.error = 0
|
||||
file_props.paused = False
|
||||
file_props.received_len = 0
|
||||
file_props.last_time = time.time()
|
||||
file_props.connected = True
|
||||
file_props.completed = False
|
||||
file_props.disconnect_cb = None
|
||||
file_props.continue_cb = None
|
||||
self._nbxmpp('IBB').send_open(to,
|
||||
file_props.transport_sid,
|
||||
4096,
|
||||
callback=self._on_open_result,
|
||||
user_data=file_props)
|
||||
return file_props
|
||||
|
||||
def _on_open_result(self, task):
|
||||
try:
|
||||
task.finish()
|
||||
except StanzaError as error:
|
||||
app.socks5queue.error_cb('Error', to_user_string(error))
|
||||
self._log.warning(error)
|
||||
return
|
||||
|
||||
file_props = task.get_user_data()
|
||||
self.send_data(file_props)
|
||||
|
||||
def send_close(self, file_props):
|
||||
file_props.connected = False
|
||||
file_props.fp.close()
|
||||
file_props.stopped = True
|
||||
to = file_props.receiver
|
||||
if file_props.direction == '<':
|
||||
to = file_props.sender
|
||||
|
||||
self._log.info('Send close to %s, sid: %s',
|
||||
to, file_props.transport_sid)
|
||||
self._nbxmpp('IBB').send_close(to, file_props.transport_sid,
|
||||
callback=self._on_close_result)
|
||||
|
||||
if file_props.completed:
|
||||
app.socks5queue.complete_transfer_cb(self._account, file_props)
|
||||
else:
|
||||
if file_props.type_ == 's':
|
||||
peerjid = file_props.receiver
|
||||
else:
|
||||
peerjid = file_props.sender
|
||||
session = self._con.get_module('Jingle').get_jingle_session(
|
||||
peerjid, file_props.sid, 'file')
|
||||
# According to the xep, the initiator also cancels
|
||||
# the jingle session if there are no more files to send using IBB
|
||||
if session.weinitiate:
|
||||
session.cancel_session()
|
||||
|
||||
def _on_close_result(self, task):
|
||||
try:
|
||||
task.finish()
|
||||
except StanzaError as error:
|
||||
app.socks5queue.error_cb('Error', to_user_string(error))
|
||||
self._log.warning(error)
|
||||
return
|
||||
|
||||
def send_data(self, file_props):
|
||||
if file_props.completed:
|
||||
self.send_close(file_props)
|
||||
return
|
||||
|
||||
chunk = file_props.fp.read(file_props.block_size)
|
||||
if chunk:
|
||||
file_props.seq += 1
|
||||
file_props.started = True
|
||||
if file_props.seq == 65536:
|
||||
file_props.seq = 0
|
||||
|
||||
self._log.info('Send data to %s, sid: %s',
|
||||
file_props.receiver, file_props.transport_sid)
|
||||
self._nbxmpp('IBB').send_data(file_props.receiver,
|
||||
file_props.transport_sid,
|
||||
file_props.seq,
|
||||
chunk,
|
||||
callback=self._on_data_result,
|
||||
user_data=file_props)
|
||||
current_time = time.time()
|
||||
file_props.elapsed_time += current_time - file_props.last_time
|
||||
file_props.last_time = current_time
|
||||
file_props.received_len += len(chunk)
|
||||
if file_props.size == file_props.received_len:
|
||||
file_props.completed = True
|
||||
app.socks5queue.progress_transfer_cb(self._account, file_props)
|
||||
|
||||
def _on_data_result(self, task):
|
||||
try:
|
||||
task.finish()
|
||||
except StanzaError as error:
|
||||
app.socks5queue.error_cb('Error', to_user_string(error))
|
||||
self._log.warning(error)
|
||||
return
|
||||
|
||||
file_props = task.get_user_data()
|
||||
self.send_data(file_props)
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return IBB(*args, **kwargs), 'IBB'
|
88
gajim/common/modules/iq.py
Normal file
88
gajim/common/modules/iq.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
# 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/>.
|
||||
|
||||
# Iq handler
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.helpers import to_user_string
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.file_props import FilesProp
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class Iq(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='iq',
|
||||
callback=self._iq_error_received,
|
||||
typ='error',
|
||||
priority=51),
|
||||
]
|
||||
|
||||
def _iq_error_received(self, _con, _stanza, properties):
|
||||
self._log.info('Error: %s', properties.error)
|
||||
if properties.error.condition in ('jid-malformed',
|
||||
'forbidden',
|
||||
'not-acceptable'):
|
||||
sid = self._get_sid(properties.id)
|
||||
file_props = FilesProp.getFileProp(self._account, sid)
|
||||
if file_props:
|
||||
if properties.error.condition == 'jid-malformed':
|
||||
file_props.error = -3
|
||||
else:
|
||||
file_props.error = -4
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('file-request-error',
|
||||
conn=self._con,
|
||||
jid=properties.jid.bare,
|
||||
file_props=file_props,
|
||||
error_msg=to_user_string(properties.error)))
|
||||
self._con.get_module('Bytestream').disconnect_transfer(
|
||||
file_props)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
if properties.error.condition == 'item-not-found':
|
||||
sid = self._get_sid(properties.id)
|
||||
file_props = FilesProp.getFileProp(self._account, sid)
|
||||
if file_props:
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('file-send-error',
|
||||
account=self._account,
|
||||
jid=str(properties.jid),
|
||||
file_props=file_props))
|
||||
self._con.get_module('Bytestream').disconnect_transfer(
|
||||
file_props)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('iq-error-received',
|
||||
account=self._account,
|
||||
properties=properties))
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
@staticmethod
|
||||
def _get_sid(id_):
|
||||
sid = id_
|
||||
if len(id_) > 3 and id_[2] == '_':
|
||||
sid = id_[3:]
|
||||
return sid
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return Iq(*args, **kwargs), 'Iq'
|
311
gajim/common/modules/jingle.py
Normal file
311
gajim/common/modules/jingle.py
Normal file
|
@ -0,0 +1,311 @@
|
|||
# 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/>.
|
||||
|
||||
"""
|
||||
Handles the jingle signalling protocol
|
||||
"""
|
||||
|
||||
#TODO:
|
||||
# * things in XEP 0176, including:
|
||||
# - http://xmpp.org/extensions/xep-0176.html#protocol-restarts
|
||||
# - http://xmpp.org/extensions/xep-0176.html#fallback
|
||||
# * XEP 0177 (raw udp)
|
||||
|
||||
# * UI:
|
||||
# - make state and codec information available to the user
|
||||
# - video integration
|
||||
# * config:
|
||||
# - codecs
|
||||
|
||||
import logging
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
|
||||
from gajim.common import helpers
|
||||
from gajim.common import app
|
||||
from gajim.common import jingle_xtls
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
from gajim.common.jingle_session import JingleSession
|
||||
from gajim.common.jingle_session import JingleStates
|
||||
from gajim.common.jingle_ft import JingleFileTransfer
|
||||
from gajim.common.jingle_transport import JingleTransportSocks5
|
||||
from gajim.common.jingle_transport import JingleTransportIBB
|
||||
from gajim.common.jingle_rtp import JingleAudio, JingleVideo
|
||||
|
||||
logger = logging.getLogger('gajim.c.m.jingle')
|
||||
|
||||
|
||||
class Jingle(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='iq',
|
||||
typ='result',
|
||||
callback=self._on_jingle_iq),
|
||||
StanzaHandler(name='iq',
|
||||
typ='error',
|
||||
callback=self._on_jingle_iq),
|
||||
StanzaHandler(name='iq',
|
||||
typ='set',
|
||||
ns=Namespace.JINGLE,
|
||||
callback=self._on_jingle_iq),
|
||||
StanzaHandler(name='iq',
|
||||
typ='get',
|
||||
ns=Namespace.PUBKEY_PUBKEY,
|
||||
callback=self._on_pubkey_request),
|
||||
StanzaHandler(name='iq',
|
||||
typ='result',
|
||||
ns=Namespace.PUBKEY_PUBKEY,
|
||||
callback=self._pubkey_result_received),
|
||||
]
|
||||
|
||||
# dictionary: sessionid => JingleSession object
|
||||
self._sessions = {}
|
||||
|
||||
# dictionary: (jid, iq stanza id) => JingleSession object,
|
||||
# one time callbacks
|
||||
self.__iq_responses = {}
|
||||
self.files = []
|
||||
|
||||
def delete_jingle_session(self, sid):
|
||||
"""
|
||||
Remove a jingle session from a jingle stanza dispatcher
|
||||
"""
|
||||
if sid in self._sessions:
|
||||
#FIXME: Move this elsewhere?
|
||||
for content in list(self._sessions[sid].contents.values()):
|
||||
content.destroy()
|
||||
self._sessions[sid].callbacks = []
|
||||
del self._sessions[sid]
|
||||
|
||||
def _on_pubkey_request(self, con, stanza, _properties):
|
||||
jid_from = helpers.get_full_jid_from_iq(stanza)
|
||||
self._log.info('Pubkey request from %s', jid_from)
|
||||
sid = stanza.getAttr('id')
|
||||
jingle_xtls.send_cert(con, jid_from, sid)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _pubkey_result_received(self, con, stanza, _properties):
|
||||
jid_from = helpers.get_full_jid_from_iq(stanza)
|
||||
self._log.info('Pubkey result from %s', jid_from)
|
||||
jingle_xtls.handle_new_cert(con, stanza, jid_from)
|
||||
|
||||
def _on_jingle_iq(self, _con, stanza, _properties):
|
||||
"""
|
||||
The jingle stanza dispatcher
|
||||
|
||||
Route jingle stanza to proper JingleSession object, or create one if it
|
||||
is a new session.
|
||||
|
||||
TODO: Also check if the stanza isn't an error stanza, if so route it
|
||||
adequately.
|
||||
"""
|
||||
# get data
|
||||
try:
|
||||
jid = helpers.get_full_jid_from_iq(stanza)
|
||||
except helpers.InvalidFormat:
|
||||
logger.warning('Invalid JID: %s, ignoring it', stanza.getFrom())
|
||||
return
|
||||
id_ = stanza.getID()
|
||||
if (jid, id_) in self.__iq_responses.keys():
|
||||
self.__iq_responses[(jid, id_)].on_stanza(stanza)
|
||||
del self.__iq_responses[(jid, id_)]
|
||||
raise nbxmpp.NodeProcessed
|
||||
jingle = stanza.getTag('jingle')
|
||||
# a jingle element is not necessary in iq-result stanza
|
||||
# don't check for that
|
||||
if jingle:
|
||||
sid = jingle.getAttr('sid')
|
||||
else:
|
||||
sid = None
|
||||
for sesn in self._sessions.values():
|
||||
if id_ in sesn.iq_ids:
|
||||
sesn.on_stanza(stanza)
|
||||
return
|
||||
# do we need to create a new jingle object
|
||||
if sid not in self._sessions:
|
||||
#TODO: tie-breaking and other things...
|
||||
newjingle = JingleSession(self._con, weinitiate=False, jid=jid,
|
||||
iq_id=id_, sid=sid)
|
||||
self._sessions[sid] = newjingle
|
||||
# we already have such session in dispatcher...
|
||||
self._sessions[sid].collect_iq_id(id_)
|
||||
self._sessions[sid].on_stanza(stanza)
|
||||
# Delete invalid/unneeded sessions
|
||||
if sid in self._sessions and \
|
||||
self._sessions[sid].state == JingleStates.ENDED:
|
||||
self.delete_jingle_session(sid)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def start_audio(self, jid):
|
||||
if self.get_jingle_session(jid, media='audio'):
|
||||
return self.get_jingle_session(jid, media='audio').sid
|
||||
jingle = self.get_jingle_session(jid, media='video')
|
||||
if jingle:
|
||||
jingle.add_content('voice', JingleAudio(jingle))
|
||||
else:
|
||||
jingle = JingleSession(self._con, weinitiate=True, jid=jid)
|
||||
self._sessions[jingle.sid] = jingle
|
||||
jingle.add_content('voice', JingleAudio(jingle))
|
||||
jingle.start_session()
|
||||
return jingle.sid
|
||||
|
||||
def start_video(self, jid):
|
||||
if self.get_jingle_session(jid, media='video'):
|
||||
return self.get_jingle_session(jid, media='video').sid
|
||||
jingle = self.get_jingle_session(jid, media='audio')
|
||||
if jingle:
|
||||
video = JingleVideo(jingle)
|
||||
jingle.add_content('video', video)
|
||||
else:
|
||||
jingle = JingleSession(self._con, weinitiate=True, jid=jid)
|
||||
self._sessions[jingle.sid] = jingle
|
||||
video = JingleVideo(jingle)
|
||||
jingle.add_content('video', video)
|
||||
jingle.start_session()
|
||||
return jingle.sid
|
||||
|
||||
def start_audio_video(self, jid):
|
||||
if self.get_jingle_session(jid, media='video'):
|
||||
return self.get_jingle_session(jid, media='video').sid
|
||||
audio_session = self.get_jingle_session(jid, media='audio')
|
||||
video_session = self.get_jingle_session(jid, media='video')
|
||||
if audio_session and video_session:
|
||||
return audio_session.sid
|
||||
if audio_session:
|
||||
video = JingleVideo(audio_session)
|
||||
audio_session.add_content('video', video)
|
||||
return audio_session.sid
|
||||
if video_session:
|
||||
audio = JingleAudio(video_session)
|
||||
video_session.add_content('audio', audio)
|
||||
return video_session.sid
|
||||
|
||||
jingle_session = JingleSession(self._con, weinitiate=True, jid=jid)
|
||||
self._sessions[jingle_session.sid] = jingle_session
|
||||
audio = JingleAudio(jingle_session)
|
||||
video = JingleVideo(jingle_session)
|
||||
jingle_session.add_content('audio', audio)
|
||||
jingle_session.add_content('video', video)
|
||||
jingle_session.start_session()
|
||||
return jingle_session.sid
|
||||
|
||||
def start_file_transfer(self, jid, file_props, request=False):
|
||||
logger.info("start file transfer with file: %s", file_props)
|
||||
contact = app.contacts.get_contact_with_highest_priority(
|
||||
self._account, app.get_jid_without_resource(jid))
|
||||
if app.contacts.is_gc_contact(self._account, jid):
|
||||
gcc = jid.split('/')
|
||||
if len(gcc) == 2:
|
||||
contact = app.contacts.get_gc_contact(self._account,
|
||||
gcc[0],
|
||||
gcc[1])
|
||||
if contact is None:
|
||||
return None
|
||||
use_security = contact.supports(Namespace.JINGLE_XTLS)
|
||||
jingle = JingleSession(self._con,
|
||||
weinitiate=True,
|
||||
jid=jid,
|
||||
werequest=request)
|
||||
# this is a file transfer
|
||||
jingle.session_type_ft = True
|
||||
self._sessions[jingle.sid] = jingle
|
||||
file_props.sid = jingle.sid
|
||||
|
||||
if contact.supports(Namespace.JINGLE_BYTESTREAM):
|
||||
transport = JingleTransportSocks5()
|
||||
elif contact.supports(Namespace.JINGLE_IBB):
|
||||
transport = JingleTransportIBB()
|
||||
else:
|
||||
transport = None
|
||||
|
||||
senders = 'initiator'
|
||||
if request:
|
||||
senders = 'responder'
|
||||
transfer = JingleFileTransfer(jingle,
|
||||
transport=transport,
|
||||
file_props=file_props,
|
||||
use_security=use_security,
|
||||
senders=senders)
|
||||
file_props.transport_sid = transport.sid
|
||||
file_props.algo = self.__hash_support(contact)
|
||||
jingle.add_content('file' + helpers.get_random_string(), transfer)
|
||||
jingle.start_session()
|
||||
return transfer.transport.sid
|
||||
|
||||
@staticmethod
|
||||
def __hash_support(contact):
|
||||
if contact.supports(Namespace.HASHES_2):
|
||||
if contact.supports(Namespace.HASHES_BLAKE2B_512):
|
||||
return 'blake2b-512'
|
||||
if contact.supports(Namespace.HASHES_BLAKE2B_256):
|
||||
return 'blake2b-256'
|
||||
if contact.supports(Namespace.HASHES_SHA3_512):
|
||||
return 'sha3-512'
|
||||
if contact.supports(Namespace.HASHES_SHA3_256):
|
||||
return 'sha3-256'
|
||||
if contact.supports(Namespace.HASHES_SHA512):
|
||||
return 'sha-512'
|
||||
if contact.supports(Namespace.HASHES_SHA256):
|
||||
return 'sha-256'
|
||||
return None
|
||||
|
||||
def get_jingle_sessions(self, jid, sid=None, media=None):
|
||||
if sid:
|
||||
return [se for se in self._sessions.values() if se.sid == sid]
|
||||
|
||||
sessions = [se for se in self._sessions.values() if se.peerjid == jid]
|
||||
if media:
|
||||
if media not in ('audio', 'video', 'file'):
|
||||
return []
|
||||
return [se for se in sessions if se.get_content(media)]
|
||||
return sessions
|
||||
|
||||
def set_file_info(self, file_):
|
||||
# Saves information about the files we have transferred
|
||||
# in case they need to be requested again.
|
||||
self.files.append(file_)
|
||||
|
||||
def get_file_info(self, peerjid, hash_=None, name=None, _account=None):
|
||||
if hash_:
|
||||
for file in self.files: # DEBUG
|
||||
#if f['hash'] == '1294809248109223':
|
||||
if file['hash'] == hash_ and file['peerjid'] == peerjid:
|
||||
return file
|
||||
elif name:
|
||||
for file in self.files:
|
||||
if file['name'] == name and file['peerjid'] == peerjid:
|
||||
return file
|
||||
return None
|
||||
|
||||
def get_jingle_session(self, jid, sid=None, media=None):
|
||||
if sid:
|
||||
if sid in self._sessions:
|
||||
return self._sessions[sid]
|
||||
return None
|
||||
if media:
|
||||
if media not in ('audio', 'video', 'file'):
|
||||
return None
|
||||
for session in self._sessions.values():
|
||||
if session.peerjid == jid and session.get_content(media):
|
||||
return session
|
||||
return None
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return Jingle(*args, **kwargs), 'Jingle'
|
59
gajim/common/modules/last_activity.py
Normal file
59
gajim/common/modules/last_activity.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
# 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-0012: Last Activity
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common import idle
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class LastActivity(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='iq',
|
||||
typ='get',
|
||||
callback=self._answer_request,
|
||||
ns=Namespace.LAST),
|
||||
]
|
||||
|
||||
def _answer_request(self, _con, stanza, properties):
|
||||
self._log.info('Request from %s', properties.jid)
|
||||
|
||||
allow_send = app.settings.get_account_setting(self._account,
|
||||
'send_idle_time')
|
||||
if app.is_installed('IDLE') and allow_send:
|
||||
iq = stanza.buildReply('result')
|
||||
query = iq.setQuery()
|
||||
seconds = idle.Monitor.get_idle_sec()
|
||||
query.attrs['seconds'] = seconds
|
||||
self._log.info('Respond with seconds: %s', seconds)
|
||||
else:
|
||||
iq = stanza.buildReply('error')
|
||||
err = nbxmpp.ErrorNode(nbxmpp.ERR_SERVICE_UNAVAILABLE)
|
||||
iq.addChild(node=err)
|
||||
|
||||
self._con.connection.send(iq)
|
||||
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return LastActivity(*args, **kwargs), 'LastActivity'
|
507
gajim/common/modules/mam.py
Normal file
507
gajim/common/modules/mam.py
Normal file
|
@ -0,0 +1,507 @@
|
|||
# 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-0313: Message Archive Management
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.util import generate_id
|
||||
from nbxmpp.errors import StanzaError
|
||||
from nbxmpp.errors import MalformedStanzaError
|
||||
from nbxmpp.errors import is_error
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
from nbxmpp.modules.util import raise_if_error
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.nec import NetworkIncomingEvent
|
||||
from gajim.common.const import ArchiveState
|
||||
from gajim.common.const import KindConstant
|
||||
from gajim.common.const import SyncThreshold
|
||||
from gajim.common.helpers import AdditionalDataDict
|
||||
from gajim.common.modules.misc import parse_oob
|
||||
from gajim.common.modules.misc import parse_correction
|
||||
from gajim.common.modules.util import get_eme_message
|
||||
from gajim.common.modules.util import as_task
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class MAM(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'MAM'
|
||||
_nbxmpp_methods = [
|
||||
'request_preferences',
|
||||
'set_preferences',
|
||||
'make_query',
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='message',
|
||||
callback=self._set_message_archive_info,
|
||||
priority=41),
|
||||
StanzaHandler(name='message',
|
||||
callback=self._mam_message_received,
|
||||
priority=51),
|
||||
]
|
||||
|
||||
self.available = False
|
||||
self._mam_query_ids = {}
|
||||
|
||||
# Holds archive jids where catch up was successful
|
||||
self._catch_up_finished = []
|
||||
|
||||
def pass_disco(self, info):
|
||||
if Namespace.MAM_2 not in info.features:
|
||||
return
|
||||
|
||||
self.available = True
|
||||
self._log.info('Discovered MAM: %s', info.jid)
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('feature-discovered',
|
||||
account=self._account,
|
||||
feature=Namespace.MAM_2))
|
||||
|
||||
def reset_state(self):
|
||||
self._mam_query_ids.clear()
|
||||
self._catch_up_finished.clear()
|
||||
|
||||
def _remove_query_id(self, jid):
|
||||
self._mam_query_ids.pop(jid, None)
|
||||
|
||||
def is_catch_up_finished(self, jid):
|
||||
return jid in self._catch_up_finished
|
||||
|
||||
def _from_valid_archive(self, _stanza, properties):
|
||||
if properties.type.is_groupchat:
|
||||
expected_archive = properties.jid
|
||||
else:
|
||||
expected_archive = self._con.get_own_jid()
|
||||
|
||||
return properties.mam.archive.bare_match(expected_archive)
|
||||
|
||||
def _get_unique_id(self, properties):
|
||||
if properties.type.is_groupchat:
|
||||
return properties.mam.id, None
|
||||
|
||||
if properties.is_self_message:
|
||||
return None, properties.id
|
||||
|
||||
if properties.is_muc_pm:
|
||||
return properties.mam.id, properties.id
|
||||
|
||||
if self._con.get_own_jid().bare_match(properties.from_):
|
||||
# message we sent
|
||||
return properties.mam.id, properties.id
|
||||
|
||||
# A message we received
|
||||
return properties.mam.id, None
|
||||
|
||||
def _set_message_archive_info(self, _con, _stanza, properties):
|
||||
if (properties.is_mam_message or
|
||||
properties.is_pubsub or
|
||||
properties.is_muc_subject):
|
||||
return
|
||||
|
||||
if properties.type.is_groupchat:
|
||||
archive_jid = properties.jid.bare
|
||||
timestamp = properties.timestamp
|
||||
|
||||
disco_info = app.storage.cache.get_last_disco_info(archive_jid)
|
||||
if disco_info is None:
|
||||
# This is the case on MUC creation
|
||||
# After MUC configuration we receive a configuration change
|
||||
# message before we had the chance to disco the new MUC
|
||||
return
|
||||
|
||||
if disco_info.mam_namespace != Namespace.MAM_2:
|
||||
return
|
||||
|
||||
else:
|
||||
if not self.available:
|
||||
return
|
||||
|
||||
archive_jid = self._con.get_own_jid().bare
|
||||
timestamp = None
|
||||
|
||||
if properties.stanza_id is None:
|
||||
return
|
||||
|
||||
if not archive_jid == properties.stanza_id.by:
|
||||
return
|
||||
|
||||
if not self.is_catch_up_finished(archive_jid):
|
||||
return
|
||||
|
||||
app.storage.archive.set_archive_infos(
|
||||
archive_jid,
|
||||
last_mam_id=properties.stanza_id.id,
|
||||
last_muc_timestamp=timestamp)
|
||||
|
||||
def _mam_message_received(self, _con, stanza, properties):
|
||||
if not properties.is_mam_message:
|
||||
return
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkIncomingEvent('mam-message-received',
|
||||
account=self._account,
|
||||
stanza=stanza,
|
||||
properties=properties))
|
||||
|
||||
if not self._from_valid_archive(stanza, properties):
|
||||
self._log.warning('Message from invalid archive %s',
|
||||
properties.mam.archive)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
self._log.info('Received message from archive: %s',
|
||||
properties.mam.archive)
|
||||
if not self._is_valid_request(properties):
|
||||
self._log.warning('Invalid MAM Message: unknown query id %s',
|
||||
properties.mam.query_id)
|
||||
self._log.debug(stanza)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
is_groupchat = properties.type.is_groupchat
|
||||
if is_groupchat:
|
||||
kind = KindConstant.GC_MSG
|
||||
else:
|
||||
if properties.from_.bare_match(self._con.get_own_jid()):
|
||||
kind = KindConstant.CHAT_MSG_SENT
|
||||
else:
|
||||
kind = KindConstant.CHAT_MSG_RECV
|
||||
|
||||
stanza_id, message_id = self._get_unique_id(properties)
|
||||
|
||||
# Search for duplicates
|
||||
if app.storage.archive.find_stanza_id(self._account,
|
||||
str(properties.mam.archive),
|
||||
stanza_id,
|
||||
message_id,
|
||||
groupchat=is_groupchat):
|
||||
self._log.info('Found duplicate with stanza-id: %s, '
|
||||
'message-id: %s', stanza_id, message_id)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
additional_data = AdditionalDataDict()
|
||||
if properties.has_user_delay:
|
||||
# Record it as a user timestamp
|
||||
additional_data.set_value(
|
||||
'gajim', 'user_timestamp', properties.user_timestamp)
|
||||
|
||||
parse_oob(properties, additional_data)
|
||||
|
||||
msgtxt = properties.body
|
||||
|
||||
if properties.is_encrypted:
|
||||
additional_data['encrypted'] = properties.encrypted.additional_data
|
||||
else:
|
||||
if properties.eme is not None:
|
||||
msgtxt = get_eme_message(properties.eme)
|
||||
|
||||
if not msgtxt:
|
||||
# For example Chatstates, Receipts, Chatmarkers
|
||||
self._log.debug(stanza.getProperties())
|
||||
return
|
||||
|
||||
with_ = properties.jid.bare
|
||||
if properties.is_muc_pm:
|
||||
# we store the message with the full JID
|
||||
with_ = str(with_)
|
||||
|
||||
if properties.is_self_message:
|
||||
# Self messages can only be deduped with origin-id
|
||||
if message_id is None:
|
||||
self._log.warning('Self message without origin-id found')
|
||||
return
|
||||
stanza_id = message_id
|
||||
|
||||
app.storage.archive.insert_into_logs(
|
||||
self._account,
|
||||
with_,
|
||||
properties.mam.timestamp,
|
||||
kind,
|
||||
unread=False,
|
||||
message=msgtxt,
|
||||
contact_name=properties.muc_nickname,
|
||||
additional_data=additional_data,
|
||||
stanza_id=stanza_id,
|
||||
message_id=properties.id)
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('mam-decrypted-message-received',
|
||||
account=self._account,
|
||||
additional_data=additional_data,
|
||||
correct_id=parse_correction(properties),
|
||||
archive_jid=properties.mam.archive,
|
||||
msgtxt=properties.body,
|
||||
properties=properties,
|
||||
kind=kind,
|
||||
)
|
||||
)
|
||||
|
||||
def _is_valid_request(self, properties):
|
||||
valid_id = self._mam_query_ids.get(properties.mam.archive, None)
|
||||
return valid_id == properties.mam.query_id
|
||||
|
||||
def _get_query_id(self, jid):
|
||||
query_id = generate_id()
|
||||
self._mam_query_ids[jid] = query_id
|
||||
return query_id
|
||||
|
||||
def _get_query_params(self):
|
||||
own_jid = self._con.get_own_jid().bare
|
||||
archive = app.storage.archive.get_archive_infos(own_jid)
|
||||
|
||||
mam_id = None
|
||||
if archive is not None:
|
||||
mam_id = archive.last_mam_id
|
||||
|
||||
start_date = None
|
||||
if mam_id:
|
||||
self._log.info('Request archive: %s, after mam-id %s',
|
||||
own_jid, mam_id)
|
||||
|
||||
else:
|
||||
# First Start, we request the last week
|
||||
start_date = datetime.utcnow() - timedelta(days=7)
|
||||
self._log.info('Request archive: %s, after date %s',
|
||||
own_jid, start_date)
|
||||
return mam_id, start_date
|
||||
|
||||
def _get_muc_query_params(self, jid, threshold):
|
||||
archive = app.storage.archive.get_archive_infos(jid)
|
||||
mam_id = None
|
||||
start_date = None
|
||||
|
||||
if archive is None or archive.last_mam_id is None:
|
||||
# First join
|
||||
start_date = datetime.utcnow() - timedelta(days=1)
|
||||
self._log.info('Request archive: %s, after date %s',
|
||||
jid, start_date)
|
||||
|
||||
elif threshold == SyncThreshold.NO_THRESHOLD:
|
||||
# Not our first join and no threshold set
|
||||
|
||||
mam_id = archive.last_mam_id
|
||||
self._log.info('Request archive: %s, after mam-id %s',
|
||||
jid, archive.last_mam_id)
|
||||
|
||||
else:
|
||||
# Not our first join, check how much time elapsed since our
|
||||
# last join and check against threshold
|
||||
last_timestamp = archive.last_muc_timestamp
|
||||
if last_timestamp is None:
|
||||
self._log.info('No last muc timestamp found: %s', jid)
|
||||
last_timestamp = 0
|
||||
|
||||
last = datetime.utcfromtimestamp(float(last_timestamp))
|
||||
if datetime.utcnow() - last > timedelta(days=threshold):
|
||||
# To much time has elapsed since last join, apply threshold
|
||||
start_date = datetime.utcnow() - timedelta(days=threshold)
|
||||
self._log.info('Too much time elapsed since last join, '
|
||||
'request archive: %s, after date %s, '
|
||||
'threshold: %s', jid, start_date, threshold)
|
||||
|
||||
else:
|
||||
# Request from last mam-id
|
||||
mam_id = archive.last_mam_id
|
||||
self._log.info('Request archive: %s, after mam-id %s:',
|
||||
jid, archive.last_mam_id)
|
||||
|
||||
return mam_id, start_date
|
||||
|
||||
@as_task
|
||||
def request_archive_on_signin(self):
|
||||
_task = yield
|
||||
|
||||
own_jid = self._con.get_own_jid().bare
|
||||
|
||||
if own_jid in self._mam_query_ids:
|
||||
self._log.warning('request already running for %s', own_jid)
|
||||
return
|
||||
|
||||
mam_id, start_date = self._get_query_params()
|
||||
|
||||
result = yield self._execute_query(own_jid, mam_id, start_date)
|
||||
if is_error(result):
|
||||
if result.condition != 'item-not-found':
|
||||
self._log.warning(result)
|
||||
return
|
||||
|
||||
app.storage.archive.reset_archive_infos(result.jid)
|
||||
_, start_date = self._get_query_params()
|
||||
result = yield self._execute_query(result.jid, None, start_date)
|
||||
if is_error(result):
|
||||
self._log.warning(result)
|
||||
return
|
||||
|
||||
if result.rsm.last is not None:
|
||||
# <last> is not provided if the requested page was empty
|
||||
# so this means we did not get anything hence we only need
|
||||
# to update the archive info if <last> is present
|
||||
app.storage.archive.set_archive_infos(
|
||||
result.jid,
|
||||
last_mam_id=result.rsm.last,
|
||||
last_muc_timestamp=time.time())
|
||||
|
||||
if start_date is not None:
|
||||
# Record the earliest timestamp we request from
|
||||
# the account archive. For the account archive we only
|
||||
# set start_date at the very first request.
|
||||
app.storage.archive.set_archive_infos(
|
||||
result.jid,
|
||||
oldest_mam_timestamp=start_date.timestamp())
|
||||
|
||||
@as_task
|
||||
def request_archive_on_muc_join(self, jid):
|
||||
_task = yield
|
||||
|
||||
threshold = app.settings.get_group_chat_setting(self._account,
|
||||
jid,
|
||||
'sync_threshold')
|
||||
self._log.info('Threshold for %s: %s', jid, threshold)
|
||||
|
||||
if threshold == SyncThreshold.NO_SYNC:
|
||||
return
|
||||
|
||||
mam_id, start_date = self._get_muc_query_params(jid, threshold)
|
||||
|
||||
result = yield self._execute_query(jid, mam_id, start_date)
|
||||
if is_error(result):
|
||||
if result.condition != 'item-not-found':
|
||||
self._log.warning(result)
|
||||
return
|
||||
|
||||
app.storage.archive.reset_archive_infos(result.jid)
|
||||
_, start_date = self._get_muc_query_params(jid, threshold)
|
||||
result = yield self._execute_query(result.jid, None, start_date)
|
||||
if is_error(result):
|
||||
self._log.warning(result)
|
||||
return
|
||||
|
||||
if result.rsm.last is not None:
|
||||
# <last> is not provided if the requested page was empty
|
||||
# so this means we did not get anything hence we only need
|
||||
# to update the archive info if <last> is present
|
||||
app.storage.archive.set_archive_infos(
|
||||
result.jid,
|
||||
last_mam_id=result.rsm.last,
|
||||
last_muc_timestamp=time.time())
|
||||
|
||||
@as_task
|
||||
def _execute_query(self, jid, mam_id, start_date):
|
||||
_task = yield
|
||||
|
||||
if jid in self._catch_up_finished:
|
||||
self._catch_up_finished.remove(jid)
|
||||
|
||||
queryid = self._get_query_id(jid)
|
||||
|
||||
result = yield self.make_query(jid,
|
||||
queryid,
|
||||
after=mam_id,
|
||||
start=start_date)
|
||||
|
||||
self._remove_query_id(result.jid)
|
||||
|
||||
raise_if_error(result)
|
||||
|
||||
while not result.complete:
|
||||
app.storage.archive.set_archive_infos(result.jid,
|
||||
last_mam_id=result.rsm.last)
|
||||
queryid = self._get_query_id(result.jid)
|
||||
|
||||
result = yield self.make_query(result.jid,
|
||||
queryid,
|
||||
after=result.rsm.last,
|
||||
start=start_date)
|
||||
|
||||
self._remove_query_id(result.jid)
|
||||
|
||||
raise_if_error(result)
|
||||
|
||||
self._catch_up_finished.append(result.jid)
|
||||
self._log.info('Request finished: %s, last mam id: %s',
|
||||
result.jid, result.rsm.last)
|
||||
yield result
|
||||
|
||||
def request_archive_interval(self,
|
||||
start_date,
|
||||
end_date,
|
||||
after=None,
|
||||
queryid=None):
|
||||
|
||||
jid = self._con.get_own_jid().bare
|
||||
|
||||
if after is None:
|
||||
self._log.info('Request interval: %s, from %s to %s',
|
||||
jid, start_date, end_date)
|
||||
else:
|
||||
self._log.info('Request page: %s, after %s', jid, after)
|
||||
|
||||
if queryid is None:
|
||||
queryid = self._get_query_id(jid)
|
||||
self._mam_query_ids[jid] = queryid
|
||||
|
||||
self.make_query(jid,
|
||||
queryid,
|
||||
after=after,
|
||||
start=start_date,
|
||||
end=end_date,
|
||||
callback=self._on_interval_result,
|
||||
user_data=(queryid, start_date, end_date))
|
||||
return queryid
|
||||
|
||||
def _on_interval_result(self, task):
|
||||
queryid, start_date, end_date = task.get_user_data()
|
||||
|
||||
try:
|
||||
result = task.finish()
|
||||
except (StanzaError, MalformedStanzaError) as error:
|
||||
self._remove_query_id(error.jid)
|
||||
return
|
||||
|
||||
self._remove_query_id(result.jid)
|
||||
|
||||
if start_date:
|
||||
timestamp = start_date.timestamp()
|
||||
else:
|
||||
timestamp = ArchiveState.ALL
|
||||
|
||||
if result.complete:
|
||||
self._log.info('Request finished: %s, last mam id: %s',
|
||||
result.jid, result.rsm.last)
|
||||
app.storage.archive.set_archive_infos(
|
||||
result.jid, oldest_mam_timestamp=timestamp)
|
||||
app.nec.push_incoming_event(NetworkEvent(
|
||||
'archiving-interval-finished',
|
||||
account=self._account,
|
||||
query_id=queryid))
|
||||
|
||||
else:
|
||||
self.request_archive_interval(start_date,
|
||||
end_date,
|
||||
result.rsm.last,
|
||||
queryid)
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return MAM(*args, **kwargs), 'MAM'
|
390
gajim/common/modules/message.py
Normal file
390
gajim/common/modules/message.py
Normal file
|
@ -0,0 +1,390 @@
|
|||
# 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/>.
|
||||
|
||||
# Message handler
|
||||
|
||||
import time
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
from nbxmpp.util import generate_id
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.helpers import AdditionalDataDict
|
||||
from gajim.common.helpers import should_log
|
||||
from gajim.common.const import KindConstant
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.modules.util import get_eme_message
|
||||
from gajim.common.modules.misc import parse_correction
|
||||
from gajim.common.modules.misc import parse_oob
|
||||
from gajim.common.modules.misc import parse_xhtml
|
||||
|
||||
|
||||
class Message(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='message',
|
||||
callback=self._check_if_unknown_contact,
|
||||
priority=41),
|
||||
StanzaHandler(name='message',
|
||||
callback=self._message_received,
|
||||
priority=50),
|
||||
StanzaHandler(name='message',
|
||||
typ='error',
|
||||
callback=self._message_error_received,
|
||||
priority=50),
|
||||
]
|
||||
|
||||
# XEPs for which this message module should not be executed
|
||||
self._message_namespaces = set([Namespace.ROSTERX,
|
||||
Namespace.IBB])
|
||||
|
||||
def _check_if_unknown_contact(self, _con, stanza, properties):
|
||||
if (properties.type.is_groupchat or
|
||||
properties.is_muc_pm or
|
||||
properties.is_self_message or
|
||||
properties.is_mam_message):
|
||||
return
|
||||
|
||||
if self._con.get_own_jid().domain == str(properties.jid):
|
||||
# Server message
|
||||
return
|
||||
|
||||
if not app.settings.get_account_setting(self._account,
|
||||
'ignore_unknown_contacts'):
|
||||
return
|
||||
|
||||
jid = properties.jid.bare
|
||||
if self._con.get_module('Roster').get_item(jid) is None:
|
||||
self._log.warning('Ignore message from unknown contact: %s', jid)
|
||||
self._log.warning(stanza)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _message_received(self, _con, stanza, properties):
|
||||
if (properties.is_mam_message or
|
||||
properties.is_pubsub or
|
||||
properties.type.is_error):
|
||||
return
|
||||
# Check if a child of the message contains any
|
||||
# namespaces that we handle in other modules.
|
||||
# nbxmpp executes less common handlers last
|
||||
if self._message_namespaces & set(stanza.getProperties()):
|
||||
return
|
||||
|
||||
self._log.info('Received from %s', stanza.getFrom())
|
||||
|
||||
app.nec.push_incoming_event(NetworkEvent(
|
||||
'raw-message-received',
|
||||
conn=self._con,
|
||||
stanza=stanza,
|
||||
account=self._account))
|
||||
|
||||
if properties.is_carbon_message and properties.carbon.is_sent:
|
||||
# Ugly, we treat the from attr as the remote jid,
|
||||
# to make that work with sent carbons we have to do this.
|
||||
# TODO: Check where in Gajim and plugins we depend on that behavior
|
||||
stanza.setFrom(stanza.getTo())
|
||||
|
||||
from_ = stanza.getFrom()
|
||||
fjid = str(from_)
|
||||
jid = from_.bare
|
||||
resource = from_.resource
|
||||
|
||||
type_ = properties.type
|
||||
|
||||
stanza_id, message_id = self._get_unique_id(properties)
|
||||
|
||||
if properties.type.is_groupchat and properties.has_server_delay:
|
||||
# Only for XEP-0045 MUC History
|
||||
# Don’t check for message text because the message could be
|
||||
# encrypted.
|
||||
if app.storage.archive.deduplicate_muc_message(
|
||||
self._account,
|
||||
properties.jid.bare,
|
||||
properties.jid.resource,
|
||||
properties.timestamp,
|
||||
properties.id):
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
if (properties.is_self_message or properties.is_muc_pm):
|
||||
archive_jid = self._con.get_own_jid().bare
|
||||
if app.storage.archive.find_stanza_id(
|
||||
self._account,
|
||||
archive_jid,
|
||||
stanza_id,
|
||||
message_id,
|
||||
properties.type.is_groupchat):
|
||||
return
|
||||
|
||||
msgtxt = properties.body
|
||||
|
||||
# TODO: remove all control UI stuff
|
||||
gc_control = app.interface.msg_win_mgr.get_gc_control(
|
||||
jid, self._account)
|
||||
if not gc_control:
|
||||
minimized = app.interface.minimized_controls[self._account]
|
||||
gc_control = minimized.get(jid)
|
||||
session = None
|
||||
if not properties.type.is_groupchat:
|
||||
if properties.is_muc_pm and properties.type.is_error:
|
||||
session = self._con.find_session(fjid, properties.thread)
|
||||
if not session:
|
||||
session = self._con.get_latest_session(fjid)
|
||||
if not session:
|
||||
session = self._con.make_new_session(
|
||||
fjid, properties.thread, type_='pm')
|
||||
else:
|
||||
session = self._con.get_or_create_session(
|
||||
fjid, properties.thread)
|
||||
|
||||
if properties.thread and not session.received_thread_id:
|
||||
session.received_thread_id = True
|
||||
|
||||
session.last_receive = time.time()
|
||||
|
||||
additional_data = AdditionalDataDict()
|
||||
|
||||
if properties.has_user_delay:
|
||||
additional_data.set_value(
|
||||
'gajim', 'user_timestamp', properties.user_timestamp)
|
||||
|
||||
parse_oob(properties, additional_data)
|
||||
parse_xhtml(properties, additional_data)
|
||||
|
||||
app.nec.push_incoming_event(NetworkEvent('update-client-info',
|
||||
account=self._account,
|
||||
jid=jid,
|
||||
resource=resource))
|
||||
|
||||
if properties.is_encrypted:
|
||||
additional_data['encrypted'] = properties.encrypted.additional_data
|
||||
else:
|
||||
if properties.eme is not None:
|
||||
msgtxt = get_eme_message(properties.eme)
|
||||
|
||||
displaymarking = None
|
||||
if properties.has_security_label:
|
||||
displaymarking = properties.security_label.displaymarking
|
||||
|
||||
event_attr = {
|
||||
'conn': self._con,
|
||||
'stanza': stanza,
|
||||
'account': self._account,
|
||||
'additional_data': additional_data,
|
||||
'fjid': fjid,
|
||||
'jid': jid,
|
||||
'resource': resource,
|
||||
'stanza_id': stanza_id,
|
||||
'unique_id': stanza_id or message_id,
|
||||
'correct_id': parse_correction(properties),
|
||||
'msgtxt': msgtxt,
|
||||
'session': session,
|
||||
'delayed': properties.user_timestamp is not None,
|
||||
'gc_control': gc_control,
|
||||
'popup': False,
|
||||
'msg_log_id': None,
|
||||
'displaymarking': displaymarking,
|
||||
'properties': properties,
|
||||
}
|
||||
|
||||
if type_.is_groupchat:
|
||||
if not msgtxt:
|
||||
return
|
||||
|
||||
event_attr.update({
|
||||
'room_jid': jid,
|
||||
})
|
||||
event = NetworkEvent('gc-message-received', **event_attr)
|
||||
app.nec.push_incoming_event(event)
|
||||
# TODO: Some plugins modify msgtxt in the GUI event
|
||||
self._log_muc_message(event)
|
||||
return
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('decrypted-message-received', **event_attr))
|
||||
|
||||
def _message_error_received(self, _con, _stanza, properties):
|
||||
jid = properties.jid
|
||||
if not properties.is_muc_pm:
|
||||
jid = jid.new_as_bare()
|
||||
|
||||
self._log.info(properties.error)
|
||||
|
||||
app.storage.archive.set_message_error(
|
||||
app.get_jid_from_account(self._account),
|
||||
jid,
|
||||
properties.id,
|
||||
properties.error)
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('message-error',
|
||||
account=self._account,
|
||||
jid=jid,
|
||||
room_jid=jid,
|
||||
message_id=properties.id,
|
||||
error=properties.error))
|
||||
|
||||
def _log_muc_message(self, event):
|
||||
self._check_for_mam_compliance(event.room_jid, event.stanza_id)
|
||||
|
||||
if (should_log(self._account, event.jid) and
|
||||
event.msgtxt and event.properties.muc_nickname):
|
||||
# if not event.nick, it means message comes from room itself
|
||||
# usually it hold description and can be send at each connection
|
||||
# so don't store it in logs
|
||||
app.storage.archive.insert_into_logs(
|
||||
self._account,
|
||||
event.jid,
|
||||
event.properties.timestamp,
|
||||
KindConstant.GC_MSG,
|
||||
message=event.msgtxt,
|
||||
contact_name=event.properties.muc_nickname,
|
||||
additional_data=event.additional_data,
|
||||
stanza_id=event.stanza_id,
|
||||
message_id=event.properties.id)
|
||||
|
||||
def _check_for_mam_compliance(self, room_jid, stanza_id):
|
||||
disco_info = app.storage.cache.get_last_disco_info(room_jid)
|
||||
if stanza_id is None and disco_info.mam_namespace == Namespace.MAM_2:
|
||||
self._log.warning('%s announces mam:2 without stanza-id', room_jid)
|
||||
|
||||
def _get_unique_id(self, properties):
|
||||
if properties.is_self_message:
|
||||
# Deduplicate self message with message-id
|
||||
return None, properties.id
|
||||
|
||||
if properties.stanza_id is None:
|
||||
return None, None
|
||||
|
||||
if properties.type.is_groupchat:
|
||||
disco_info = app.storage.cache.get_last_disco_info(
|
||||
properties.jid.bare)
|
||||
|
||||
if disco_info.mam_namespace != Namespace.MAM_2:
|
||||
return None, None
|
||||
|
||||
archive = properties.jid
|
||||
else:
|
||||
if not self._con.get_module('MAM').available:
|
||||
return None, None
|
||||
|
||||
archive = self._con.get_own_jid()
|
||||
|
||||
if archive.bare_match(properties.stanza_id.by):
|
||||
return properties.stanza_id.id, None
|
||||
# stanza-id not added by the archive, ignore it.
|
||||
return None, None
|
||||
|
||||
def build_message_stanza(self, message):
|
||||
own_jid = self._con.get_own_jid()
|
||||
|
||||
stanza = nbxmpp.Message(to=message.jid,
|
||||
body=message.message,
|
||||
typ=message.type_,
|
||||
subject=message.subject,
|
||||
xhtml=message.xhtml)
|
||||
|
||||
if message.correct_id:
|
||||
stanza.setTag('replace', attrs={'id': message.correct_id},
|
||||
namespace=Namespace.CORRECT)
|
||||
|
||||
# XEP-0359
|
||||
message.message_id = generate_id()
|
||||
stanza.setID(message.message_id)
|
||||
stanza.setOriginID(message.message_id)
|
||||
|
||||
if message.label:
|
||||
stanza.addChild(node=message.label.to_node())
|
||||
|
||||
# XEP-0172: user_nickname
|
||||
if message.user_nick:
|
||||
stanza.setTag('nick', namespace=Namespace.NICK).setData(
|
||||
message.user_nick)
|
||||
|
||||
# XEP-0203
|
||||
# TODO: Seems delayed is not set anywhere
|
||||
if message.delayed:
|
||||
timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ',
|
||||
time.gmtime(message.delayed))
|
||||
stanza.addChild('delay',
|
||||
namespace=Namespace.DELAY2,
|
||||
attrs={'from': str(own_jid), 'stamp': timestamp})
|
||||
|
||||
# XEP-0224
|
||||
if message.attention:
|
||||
stanza.setTag('attention', namespace=Namespace.ATTENTION)
|
||||
|
||||
# XEP-0066
|
||||
if message.oob_url is not None:
|
||||
oob = stanza.addChild('x', namespace=Namespace.X_OOB)
|
||||
oob.addChild('url').setData(message.oob_url)
|
||||
|
||||
# XEP-0184
|
||||
if not own_jid.bare_match(message.jid):
|
||||
if message.message and not message.is_groupchat:
|
||||
stanza.setReceiptRequest()
|
||||
|
||||
# Mark Message as MUC PM
|
||||
if message.contact.is_pm_contact:
|
||||
stanza.setTag('x', namespace=Namespace.MUC_USER)
|
||||
|
||||
# XEP-0085
|
||||
if message.chatstate is not None:
|
||||
stanza.setTag(message.chatstate, namespace=Namespace.CHATSTATES)
|
||||
if not message.message:
|
||||
stanza.setTag('no-store',
|
||||
namespace=Namespace.MSG_HINTS)
|
||||
|
||||
# XEP-0333
|
||||
if message.message:
|
||||
stanza.setMarkable()
|
||||
if message.marker:
|
||||
marker, id_ = message.marker
|
||||
stanza.setMarker(marker, id_)
|
||||
|
||||
# Add other nodes
|
||||
if message.nodes is not None:
|
||||
for node in message.nodes:
|
||||
stanza.addChild(node=node)
|
||||
|
||||
return stanza
|
||||
|
||||
def log_message(self, message):
|
||||
if not message.is_loggable:
|
||||
return
|
||||
|
||||
if not should_log(self._account, message.jid):
|
||||
return
|
||||
|
||||
if message.message is None:
|
||||
return
|
||||
|
||||
app.storage.archive.insert_into_logs(
|
||||
self._account,
|
||||
message.jid,
|
||||
message.timestamp,
|
||||
message.kind,
|
||||
message=message.message,
|
||||
subject=message.subject,
|
||||
additional_data=message.additional_data,
|
||||
message_id=message.message_id,
|
||||
stanza_id=message.message_id)
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return Message(*args, **kwargs), 'Message'
|
107
gajim/common/modules/metacontacts.py
Normal file
107
gajim/common/modules/metacontacts.py
Normal file
|
@ -0,0 +1,107 @@
|
|||
# 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-0209: Metacontacts
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common import helpers
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class MetaContacts(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.available = False
|
||||
|
||||
def get_metacontacts(self):
|
||||
if not app.settings.get('metacontacts_enabled'):
|
||||
self._con.connect_machine()
|
||||
return
|
||||
|
||||
self._log.info('Request')
|
||||
node = nbxmpp.Node('storage', attrs={'xmlns': 'storage:metacontacts'})
|
||||
iq = nbxmpp.Iq('get', Namespace.PRIVATE, payload=node)
|
||||
|
||||
self._con.connection.SendAndCallForResponse(
|
||||
iq, self._metacontacts_received)
|
||||
|
||||
def _metacontacts_received(self, _nbxmpp_client, stanza):
|
||||
if not nbxmpp.isResultNode(stanza):
|
||||
self._log.info('Request error: %s', stanza.getError())
|
||||
else:
|
||||
self.available = True
|
||||
meta_list = self._parse_metacontacts(stanza)
|
||||
|
||||
self._log.info('Received: %s', meta_list)
|
||||
|
||||
app.nec.push_incoming_event(NetworkEvent(
|
||||
'metacontacts-received', conn=self._con, meta_list=meta_list))
|
||||
|
||||
self._con.connect_machine()
|
||||
|
||||
@staticmethod
|
||||
def _parse_metacontacts(stanza):
|
||||
meta_list = {}
|
||||
query = stanza.getQuery()
|
||||
storage = query.getTag('storage')
|
||||
metas = storage.getTags('meta')
|
||||
for meta in metas:
|
||||
try:
|
||||
jid = helpers.parse_jid(meta.getAttr('jid'))
|
||||
except helpers.InvalidFormat:
|
||||
continue
|
||||
tag = meta.getAttr('tag')
|
||||
data = {'jid': jid}
|
||||
order = meta.getAttr('order')
|
||||
try:
|
||||
order = int(order)
|
||||
except Exception:
|
||||
order = 0
|
||||
if order is not None:
|
||||
data['order'] = order
|
||||
if tag in meta_list:
|
||||
meta_list[tag].append(data)
|
||||
else:
|
||||
meta_list[tag] = [data]
|
||||
return meta_list
|
||||
|
||||
def store_metacontacts(self, tags_list):
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
iq = nbxmpp.Iq('set', Namespace.PRIVATE)
|
||||
meta = iq.getQuery().addChild('storage',
|
||||
namespace='storage:metacontacts')
|
||||
for tag in tags_list:
|
||||
for data in tags_list[tag]:
|
||||
jid = data['jid']
|
||||
dict_ = {'jid': jid, 'tag': tag}
|
||||
if 'order' in data:
|
||||
dict_['order'] = data['order']
|
||||
meta.addChild(name='meta', attrs=dict_)
|
||||
self._log.info('Store: %s', tags_list)
|
||||
self._con.connection.SendAndCallForResponse(
|
||||
iq, self._store_response_received)
|
||||
|
||||
def _store_response_received(self, _nbxmpp_client, stanza):
|
||||
if not nbxmpp.isResultNode(stanza):
|
||||
self._log.info('Store error: %s', stanza.getError())
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return MetaContacts(*args, **kwargs), 'MetaContacts'
|
51
gajim/common/modules/misc.py
Normal file
51
gajim/common/modules/misc.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
# 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/>.
|
||||
|
||||
# All XEPs that don’t need their own module
|
||||
|
||||
import logging
|
||||
|
||||
from gajim.common.i18n import get_rfc5646_lang
|
||||
|
||||
log = logging.getLogger('gajim.c.m.misc')
|
||||
|
||||
|
||||
# XEP-0066: Out of Band Data
|
||||
|
||||
def parse_oob(properties, additional_data):
|
||||
if not properties.is_oob:
|
||||
return
|
||||
|
||||
additional_data.set_value('gajim', 'oob_url', properties.oob.url)
|
||||
if properties.oob.desc is not None:
|
||||
additional_data.set_value('gajim', 'oob_desc',
|
||||
properties.oob.desc)
|
||||
|
||||
|
||||
# XEP-0308: Last Message Correction
|
||||
|
||||
def parse_correction(properties):
|
||||
if not properties.is_correction:
|
||||
return None
|
||||
return properties.correction.id
|
||||
|
||||
|
||||
# XEP-0071: XHTML-IM
|
||||
|
||||
def parse_xhtml(properties, additional_data):
|
||||
if not properties.has_xhtml:
|
||||
return
|
||||
|
||||
body = properties.xhtml.get_body(get_rfc5646_lang())
|
||||
additional_data.set_value('gajim', 'xhtml', body)
|
857
gajim/common/modules/muc.py
Normal file
857
gajim/common/modules/muc.py
Normal file
|
@ -0,0 +1,857 @@
|
|||
# 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-0045: Multi-User Chat
|
||||
# XEP-0249: Direct MUC Invitations
|
||||
|
||||
import logging
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.const import InviteType
|
||||
from nbxmpp.const import PresenceType
|
||||
from nbxmpp.const import StatusCode
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
from nbxmpp.errors import StanzaError
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common import helpers
|
||||
from gajim.common import ged
|
||||
from gajim.common.const import KindConstant
|
||||
from gajim.common.const import MUCJoinedState
|
||||
from gajim.common.helpers import AdditionalDataDict
|
||||
from gajim.common.helpers import get_default_muc_config
|
||||
from gajim.common.helpers import to_user_string
|
||||
from gajim.common.helpers import event_filter
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.modules.bits_of_binary import store_bob_data
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
log = logging.getLogger('gajim.c.m.muc')
|
||||
|
||||
|
||||
class MUC(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'MUC'
|
||||
_nbxmpp_methods = [
|
||||
'get_affiliation',
|
||||
'set_role',
|
||||
'set_affiliation',
|
||||
'set_config',
|
||||
'set_subject',
|
||||
'cancel_config',
|
||||
'send_captcha',
|
||||
'cancel_captcha',
|
||||
'decline',
|
||||
'invite',
|
||||
'request_config',
|
||||
'request_voice',
|
||||
'approve_voice_request',
|
||||
'destroy',
|
||||
'request_disco_info'
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='presence',
|
||||
callback=self._on_muc_user_presence,
|
||||
ns=Namespace.MUC_USER,
|
||||
priority=49),
|
||||
StanzaHandler(name='presence',
|
||||
callback=self._on_error_presence,
|
||||
typ='error',
|
||||
priority=49),
|
||||
StanzaHandler(name='message',
|
||||
callback=self._on_subject_change,
|
||||
typ='groupchat',
|
||||
priority=49),
|
||||
StanzaHandler(name='message',
|
||||
callback=self._on_config_change,
|
||||
ns=Namespace.MUC_USER,
|
||||
priority=49),
|
||||
StanzaHandler(name='message',
|
||||
callback=self._on_invite_or_decline,
|
||||
typ='normal',
|
||||
ns=Namespace.MUC_USER,
|
||||
priority=49),
|
||||
StanzaHandler(name='message',
|
||||
callback=self._on_invite_or_decline,
|
||||
ns=Namespace.CONFERENCE,
|
||||
priority=49),
|
||||
StanzaHandler(name='message',
|
||||
callback=self._on_captcha_challenge,
|
||||
ns=Namespace.CAPTCHA,
|
||||
priority=49),
|
||||
StanzaHandler(name='message',
|
||||
callback=self._on_voice_request,
|
||||
ns=Namespace.DATA,
|
||||
priority=49)
|
||||
]
|
||||
|
||||
self.register_events([
|
||||
('account-disconnected', ged.CORE, self._on_account_disconnected),
|
||||
])
|
||||
|
||||
self._manager = MUCManager(self._log)
|
||||
self._rejoin_muc = set()
|
||||
self._join_timeouts = {}
|
||||
self._rejoin_timeouts = {}
|
||||
self._muc_service_jid = None
|
||||
|
||||
@property
|
||||
def supported(self):
|
||||
return self._muc_service_jid is not None
|
||||
|
||||
@property
|
||||
def service_jid(self):
|
||||
return self._muc_service_jid
|
||||
|
||||
def get_manager(self):
|
||||
return self._manager
|
||||
|
||||
def pass_disco(self, info):
|
||||
for identity in info.identities:
|
||||
if identity.category != 'conference':
|
||||
continue
|
||||
if identity.type != 'text':
|
||||
continue
|
||||
if Namespace.MUC in info.features:
|
||||
self._log.info('Discovered MUC: %s', info.jid)
|
||||
self._muc_service_jid = info.jid
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def join(self, muc_data):
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
|
||||
self._manager.add(muc_data)
|
||||
|
||||
disco_info = app.storage.cache.get_last_disco_info(muc_data.jid,
|
||||
max_age=60)
|
||||
if disco_info is None:
|
||||
self._con.get_module('Discovery').disco_muc(
|
||||
muc_data.jid,
|
||||
callback=self._on_disco_result)
|
||||
else:
|
||||
self._join(muc_data)
|
||||
|
||||
def create(self, muc_data):
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
|
||||
self._manager.add(muc_data)
|
||||
self._create(muc_data)
|
||||
|
||||
def _on_disco_result(self, task):
|
||||
try:
|
||||
result = task.finish()
|
||||
except StanzaError as error:
|
||||
self._log.info('Disco %s failed: %s', error.jid, error.get_text())
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('muc-join-failed',
|
||||
account=self._account,
|
||||
room_jid=error.jid.bare,
|
||||
error=error))
|
||||
return
|
||||
|
||||
muc_data = self._manager.get(result.info.jid)
|
||||
if muc_data is None:
|
||||
self._log.warning('MUC Data not found, join aborted')
|
||||
return
|
||||
self._join(muc_data)
|
||||
|
||||
def _join(self, muc_data):
|
||||
presence = self._con.get_module('Presence').get_presence(
|
||||
muc_data.occupant_jid,
|
||||
show=self._con.status,
|
||||
status=self._con.status_message)
|
||||
|
||||
muc_x = presence.setTag(Namespace.MUC + ' x')
|
||||
muc_x.setTag('history', {'maxchars': '0'})
|
||||
|
||||
if muc_data.password is not None:
|
||||
muc_x.setTagData('password', muc_data.password)
|
||||
|
||||
self._log.info('Join MUC: %s', muc_data.jid)
|
||||
self._manager.set_state(muc_data.jid, MUCJoinedState.JOINING)
|
||||
self._con.connection.send(presence)
|
||||
|
||||
def _rejoin(self, room_jid):
|
||||
muc_data = self._manager.get(room_jid)
|
||||
if muc_data.state == MUCJoinedState.NOT_JOINED:
|
||||
self._log.info('Rejoin %s', room_jid)
|
||||
self._join(muc_data)
|
||||
return True
|
||||
|
||||
def _create(self, muc_data):
|
||||
presence = self._con.get_module('Presence').get_presence(
|
||||
muc_data.occupant_jid,
|
||||
show=self._con.status,
|
||||
status=self._con.status_message)
|
||||
|
||||
presence.setTag(Namespace.MUC + ' x')
|
||||
|
||||
self._log.info('Create MUC: %s', muc_data.jid)
|
||||
self._manager.set_state(muc_data.jid, MUCJoinedState.CREATING)
|
||||
self._con.connection.send(presence)
|
||||
|
||||
def leave(self, room_jid, reason=None):
|
||||
self._log.info('Leave MUC: %s', room_jid)
|
||||
self._remove_join_timeout(room_jid)
|
||||
self._remove_rejoin_timeout(room_jid)
|
||||
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
|
||||
muc_data = self._manager.get(room_jid)
|
||||
self._con.get_module('Presence').send_presence(
|
||||
muc_data.occupant_jid,
|
||||
typ='unavailable',
|
||||
status=reason,
|
||||
caps=False)
|
||||
# We leave a group chat, disable bookmark autojoin
|
||||
self._con.get_module('Bookmarks').modify(room_jid, autojoin=False)
|
||||
|
||||
def configure_room(self, room_jid):
|
||||
self._nbxmpp('MUC').request_config(room_jid,
|
||||
callback=self._on_room_config)
|
||||
|
||||
def _on_room_config(self, task):
|
||||
try:
|
||||
result = task.finish()
|
||||
except StanzaError as error:
|
||||
self._log.info(error)
|
||||
app.nec.push_incoming_event(NetworkEvent(
|
||||
'muc-configuration-failed',
|
||||
account=self._account,
|
||||
room_jid=error.jid,
|
||||
error=error))
|
||||
return
|
||||
|
||||
self._log.info('Configure room: %s', result.jid)
|
||||
|
||||
muc_data = self._manager.get(result.jid)
|
||||
self._apply_config(result.form, muc_data.config)
|
||||
self.set_config(result.jid,
|
||||
result.form,
|
||||
callback=self._on_config_result)
|
||||
|
||||
@staticmethod
|
||||
def _apply_config(form, config=None):
|
||||
default_config = get_default_muc_config()
|
||||
if config is not None:
|
||||
default_config.update(config)
|
||||
for var, value in default_config.items():
|
||||
try:
|
||||
field = form[var]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
field.value = value
|
||||
|
||||
def _on_config_result(self, task):
|
||||
try:
|
||||
result = task.finish()
|
||||
except StanzaError as error:
|
||||
self._log.info(error)
|
||||
app.nec.push_incoming_event(NetworkEvent(
|
||||
'muc-configuration-failed',
|
||||
account=self._account,
|
||||
room_jid=error.jid,
|
||||
error=error))
|
||||
return
|
||||
|
||||
self._con.get_module('Discovery').disco_muc(
|
||||
result.jid, callback=self._on_disco_result_after_config)
|
||||
|
||||
# If this is an automatic room creation
|
||||
try:
|
||||
invites = app.automatic_rooms[self._account][result.jid]['invities']
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
user_list = {}
|
||||
for jid in invites:
|
||||
user_list[jid] = {'affiliation': 'member'}
|
||||
self.set_affiliation(result.jid, user_list)
|
||||
|
||||
for jid in invites:
|
||||
self.invite(result.jid, jid)
|
||||
|
||||
def _on_disco_result_after_config(self, task):
|
||||
try:
|
||||
result = task.finish()
|
||||
except StanzaError as error:
|
||||
self._log.info('Disco %s failed: %s', error.jid, error.get_text())
|
||||
return
|
||||
|
||||
jid = result.info.jid
|
||||
muc_data = self._manager.get(jid)
|
||||
self._room_join_complete(muc_data)
|
||||
|
||||
self._log.info('Configuration finished: %s', jid)
|
||||
app.nec.push_incoming_event(NetworkEvent(
|
||||
'muc-configuration-finished',
|
||||
account=self._account,
|
||||
room_jid=jid))
|
||||
|
||||
def update_presence(self):
|
||||
mucs = self._manager.get_mucs_with_state([MUCJoinedState.JOINED,
|
||||
MUCJoinedState.JOINING])
|
||||
|
||||
status, message, idle = self._con.get_presence_state()
|
||||
for muc_data in mucs:
|
||||
self._con.get_module('Presence').send_presence(
|
||||
muc_data.occupant_jid,
|
||||
show=status,
|
||||
status=message,
|
||||
idle_time=idle)
|
||||
|
||||
def change_nick(self, room_jid, new_nick):
|
||||
status, message, _idle = self._con.get_presence_state()
|
||||
self._con.get_module('Presence').send_presence(
|
||||
'%s/%s' % (room_jid, new_nick),
|
||||
show=status,
|
||||
status=message)
|
||||
|
||||
def _on_error_presence(self, _con, _stanza, properties):
|
||||
room_jid = properties.jid.bare
|
||||
muc_data = self._manager.get(room_jid)
|
||||
if muc_data is None:
|
||||
return
|
||||
|
||||
if muc_data.state == MUCJoinedState.JOINING:
|
||||
if properties.error.condition == 'conflict':
|
||||
self._remove_rejoin_timeout(room_jid)
|
||||
muc_data.nick += '_'
|
||||
self._log.info('Nickname conflict: %s change to %s',
|
||||
muc_data.jid, muc_data.nick)
|
||||
self._join(muc_data)
|
||||
elif properties.error.condition == 'not-authorized':
|
||||
self._remove_rejoin_timeout(room_jid)
|
||||
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
|
||||
self._raise_muc_event('muc-password-required', properties)
|
||||
else:
|
||||
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
|
||||
if room_jid not in self._rejoin_muc:
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('muc-join-failed',
|
||||
account=self._account,
|
||||
room_jid=room_jid,
|
||||
error=properties.error))
|
||||
|
||||
elif muc_data.state == MUCJoinedState.CREATING:
|
||||
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('muc-creation-failed',
|
||||
account=self._account,
|
||||
room_jid=room_jid,
|
||||
error=properties.error))
|
||||
|
||||
elif muc_data.state == MUCJoinedState.CAPTCHA_REQUEST:
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('muc-captcha-error',
|
||||
account=self._account,
|
||||
room_jid=room_jid,
|
||||
error_text=to_user_string(properties.error)))
|
||||
self._manager.set_state(room_jid, MUCJoinedState.CAPTCHA_FAILED)
|
||||
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
|
||||
|
||||
elif muc_data.state == MUCJoinedState.CAPTCHA_FAILED:
|
||||
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
|
||||
|
||||
else:
|
||||
self._raise_muc_event('muc-presence-error', properties)
|
||||
|
||||
def _on_muc_user_presence(self, _con, stanza, properties):
|
||||
if properties.type == PresenceType.ERROR:
|
||||
return
|
||||
|
||||
room_jid = str(properties.muc_jid)
|
||||
if room_jid not in self._manager:
|
||||
self._log.warning('Presence from unknown MUC')
|
||||
self._log.warning(stanza)
|
||||
return
|
||||
|
||||
muc_data = self._manager.get(room_jid)
|
||||
|
||||
if properties.is_muc_destroyed:
|
||||
for contact in app.contacts.get_gc_contact_list(
|
||||
self._account, room_jid):
|
||||
contact.presence = PresenceType.UNAVAILABLE
|
||||
self._log.info('MUC destroyed: %s', room_jid)
|
||||
self._remove_join_timeout(room_jid)
|
||||
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
|
||||
self._raise_muc_event('muc-destroyed', properties)
|
||||
return
|
||||
|
||||
contact = app.contacts.get_gc_contact(self._account,
|
||||
room_jid,
|
||||
properties.muc_nickname)
|
||||
|
||||
if properties.is_nickname_changed:
|
||||
if properties.is_muc_self_presence:
|
||||
muc_data.nick = properties.muc_user.nick
|
||||
self._con.get_module('Bookmarks').modify(muc_data.jid,
|
||||
nick=muc_data.nick)
|
||||
app.contacts.remove_gc_contact(self._account, contact)
|
||||
contact.name = properties.muc_user.nick
|
||||
app.contacts.add_gc_contact(self._account, contact)
|
||||
initiator = 'Server' if properties.is_nickname_modified else 'User'
|
||||
self._log.info('%s nickname changed: %s to %s',
|
||||
initiator,
|
||||
properties.jid,
|
||||
properties.muc_user.nick)
|
||||
self._raise_muc_event('muc-nickname-changed', properties)
|
||||
return
|
||||
|
||||
if contact is None and properties.type.is_available:
|
||||
self._add_new_muc_contact(properties)
|
||||
if properties.is_muc_self_presence:
|
||||
self._log.info('Self presence: %s', properties.jid)
|
||||
if muc_data.state == MUCJoinedState.JOINING:
|
||||
if (properties.is_nickname_modified or
|
||||
muc_data.nick != properties.muc_nickname):
|
||||
muc_data.nick = properties.muc_nickname
|
||||
self._log.info('Server modified nickname to: %s',
|
||||
properties.muc_nickname)
|
||||
|
||||
elif muc_data.state == MUCJoinedState.CREATING:
|
||||
if properties.is_new_room:
|
||||
self.configure_room(room_jid)
|
||||
|
||||
self._start_join_timeout(room_jid)
|
||||
self._raise_muc_event('muc-self-presence', properties)
|
||||
|
||||
else:
|
||||
self._log.info('User joined: %s', properties.jid)
|
||||
self._raise_muc_event('muc-user-joined', properties)
|
||||
return
|
||||
|
||||
if properties.is_muc_self_presence and properties.is_kicked:
|
||||
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
|
||||
self._raise_muc_event('muc-self-kicked', properties)
|
||||
status_codes = properties.muc_status_codes or []
|
||||
if StatusCode.REMOVED_SERVICE_SHUTDOWN in status_codes:
|
||||
self._start_rejoin_timeout(room_jid)
|
||||
return
|
||||
|
||||
if properties.is_muc_self_presence and properties.type.is_unavailable:
|
||||
# Its not a kick, so this is the reflection of our own
|
||||
# unavailable presence, because we left the MUC
|
||||
return
|
||||
|
||||
if properties.type.is_unavailable:
|
||||
for _event in app.events.get_events(self._account,
|
||||
jid=str(properties.jid),
|
||||
types=['pm']):
|
||||
contact.show = properties.show
|
||||
contact.presence = properties.type
|
||||
contact.status = properties.status
|
||||
contact.affiliation = properties.affiliation
|
||||
app.interface.handle_event(self._account,
|
||||
str(properties.jid),
|
||||
'pm')
|
||||
# Handle only the first pm event, the rest will be
|
||||
# handled by the opened ChatControl
|
||||
break
|
||||
|
||||
if contact is None:
|
||||
# If contact is None, its probably that a user left from a not
|
||||
# insync MUC, can happen on older servers
|
||||
self._log.warning('Unknown contact left groupchat: %s',
|
||||
properties.jid)
|
||||
else:
|
||||
# We remove the contact from the MUC, but there could be
|
||||
# a PrivateChatControl open, so we update the contacts presence
|
||||
contact.presence = properties.type
|
||||
app.contacts.remove_gc_contact(self._account, contact)
|
||||
self._log.info('User %s left', properties.jid)
|
||||
self._raise_muc_event('muc-user-left', properties)
|
||||
return
|
||||
|
||||
if contact.affiliation != properties.affiliation:
|
||||
contact.affiliation = properties.affiliation
|
||||
self._log.info('Affiliation changed: %s %s',
|
||||
properties.jid,
|
||||
properties.affiliation)
|
||||
self._raise_muc_event('muc-user-affiliation-changed', properties)
|
||||
|
||||
if contact.role != properties.role:
|
||||
contact.role = properties.role
|
||||
self._log.info('Role changed: %s %s',
|
||||
properties.jid,
|
||||
properties.role)
|
||||
self._raise_muc_event('muc-user-role-changed', properties)
|
||||
|
||||
if (contact.status != properties.status or
|
||||
contact.show != properties.show):
|
||||
contact.status = properties.status
|
||||
contact.show = properties.show
|
||||
self._log.info('Show/Status changed: %s %s %s',
|
||||
properties.jid,
|
||||
properties.status,
|
||||
properties.show)
|
||||
self._raise_muc_event('muc-user-status-show-changed', properties)
|
||||
|
||||
def _start_rejoin_timeout(self, room_jid):
|
||||
self._remove_rejoin_timeout(room_jid)
|
||||
self._rejoin_muc.add(room_jid)
|
||||
self._log.info('Start rejoin timeout for: %s', room_jid)
|
||||
id_ = GLib.timeout_add_seconds(2, self._rejoin, room_jid)
|
||||
self._rejoin_timeouts[room_jid] = id_
|
||||
|
||||
def _remove_rejoin_timeout(self, room_jid):
|
||||
self._rejoin_muc.discard(room_jid)
|
||||
id_ = self._rejoin_timeouts.get(room_jid)
|
||||
if id_ is not None:
|
||||
self._log.info('Remove rejoin timeout for: %s', room_jid)
|
||||
GLib.source_remove(id_)
|
||||
del self._rejoin_timeouts[room_jid]
|
||||
|
||||
def _start_join_timeout(self, room_jid):
|
||||
self._remove_join_timeout(room_jid)
|
||||
self._log.info('Start join timeout for: %s', room_jid)
|
||||
id_ = GLib.timeout_add_seconds(
|
||||
10, self._fake_subject_change, room_jid)
|
||||
self._join_timeouts[room_jid] = id_
|
||||
|
||||
def _remove_join_timeout(self, room_jid):
|
||||
id_ = self._join_timeouts.get(room_jid)
|
||||
if id_ is not None:
|
||||
self._log.info('Remove join timeout for: %s', room_jid)
|
||||
GLib.source_remove(id_)
|
||||
del self._join_timeouts[room_jid]
|
||||
|
||||
def _raise_muc_event(self, event_name, properties):
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent(event_name,
|
||||
account=self._account,
|
||||
room_jid=properties.jid.bare,
|
||||
properties=properties))
|
||||
self._log_muc_event(event_name, properties)
|
||||
|
||||
def _log_muc_event(self, event_name, properties):
|
||||
if event_name not in ['muc-user-joined',
|
||||
'muc-user-left',
|
||||
'muc-user-status-show-changed']:
|
||||
return
|
||||
|
||||
if (not app.settings.get('log_contact_status_changes') or
|
||||
not helpers.should_log(self._account, properties.jid)):
|
||||
return
|
||||
|
||||
additional_data = AdditionalDataDict()
|
||||
if properties.muc_user is not None:
|
||||
if properties.muc_user.jid is not None:
|
||||
additional_data.set_value(
|
||||
'gajim', 'real_jid', str(properties.muc_user.jid))
|
||||
|
||||
# TODO: Refactor
|
||||
if properties.type == PresenceType.UNAVAILABLE:
|
||||
show = 'offline'
|
||||
else:
|
||||
show = properties.show.value
|
||||
show = app.storage.archive.convert_show_values_to_db_api_values(show)
|
||||
|
||||
app.storage.archive.insert_into_logs(
|
||||
self._account,
|
||||
properties.jid.bare,
|
||||
properties.timestamp,
|
||||
KindConstant.GCSTATUS,
|
||||
contact_name=properties.muc_nickname,
|
||||
message=properties.status or None,
|
||||
show=show,
|
||||
additional_data=additional_data)
|
||||
|
||||
def _add_new_muc_contact(self, properties):
|
||||
real_jid = None
|
||||
if properties.muc_user.jid is not None:
|
||||
real_jid = str(properties.muc_user.jid)
|
||||
contact = app.contacts.create_gc_contact(
|
||||
room_jid=properties.jid.bare,
|
||||
account=self._account,
|
||||
name=properties.muc_nickname,
|
||||
show=properties.show,
|
||||
status=properties.status,
|
||||
presence=properties.type,
|
||||
role=properties.role,
|
||||
affiliation=properties.affiliation,
|
||||
jid=real_jid,
|
||||
avatar_sha=properties.avatar_sha)
|
||||
app.contacts.add_gc_contact(self._account, contact)
|
||||
|
||||
def _on_subject_change(self, _con, _stanza, properties):
|
||||
if not properties.is_muc_subject:
|
||||
return
|
||||
|
||||
self._handle_subject_change(str(properties.muc_jid),
|
||||
properties.subject,
|
||||
properties.muc_nickname,
|
||||
properties.user_timestamp)
|
||||
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _fake_subject_change(self, room_jid):
|
||||
# This is for servers which don’t send empty subjects as part of the
|
||||
# event order on joining a MUC. For example jabber.ru
|
||||
self._log.warning('Fake subject received for %s', room_jid)
|
||||
del self._join_timeouts[room_jid]
|
||||
self._handle_subject_change(room_jid, None, None, None)
|
||||
|
||||
def _handle_subject_change(self, room_jid, subject, nickname, timestamp):
|
||||
contact = app.contacts.get_groupchat_contact(self._account, room_jid)
|
||||
if contact is None:
|
||||
return
|
||||
|
||||
contact.status = subject
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('muc-subject',
|
||||
account=self._account,
|
||||
room_jid=room_jid,
|
||||
subject=subject,
|
||||
nickname=nickname,
|
||||
user_timestamp=timestamp,
|
||||
is_fake=subject is None))
|
||||
|
||||
muc_data = self._manager.get(room_jid)
|
||||
if muc_data.state == MUCJoinedState.JOINING:
|
||||
self._room_join_complete(muc_data)
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('muc-joined',
|
||||
account=self._account,
|
||||
room_jid=muc_data.jid))
|
||||
|
||||
def _room_join_complete(self, muc_data):
|
||||
self._remove_join_timeout(muc_data.jid)
|
||||
self._manager.set_state(muc_data.jid, MUCJoinedState.JOINED)
|
||||
self._remove_rejoin_timeout(muc_data.jid)
|
||||
|
||||
# We successfully joined a MUC, set add bookmark with autojoin
|
||||
self._con.get_module('Bookmarks').add_or_modify(
|
||||
muc_data.jid,
|
||||
autojoin=True,
|
||||
password=muc_data.password,
|
||||
nick=muc_data.nick)
|
||||
|
||||
def _on_voice_request(self, _con, _stanza, properties):
|
||||
if not properties.is_voice_request:
|
||||
return
|
||||
|
||||
jid = str(properties.jid)
|
||||
contact = app.contacts.get_groupchat_contact(self._account, jid)
|
||||
if contact is None:
|
||||
return
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('muc-voice-request',
|
||||
account=self._account,
|
||||
room_jid=str(properties.muc_jid),
|
||||
voice_request=properties.voice_request))
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _on_captcha_challenge(self, _con, _stanza, properties):
|
||||
if not properties.is_captcha_challenge:
|
||||
return
|
||||
|
||||
if properties.is_mam_message:
|
||||
# Some servers store captcha challenges in MAM, don’t process them
|
||||
self._log.warning('Ignore captcha challenge received from MAM')
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
muc_data = self._manager.get(properties.jid)
|
||||
if muc_data is None:
|
||||
return
|
||||
|
||||
if muc_data.state != MUCJoinedState.JOINING:
|
||||
self._log.warning('Received captcha request but state != %s',
|
||||
MUCJoinedState.JOINING)
|
||||
return
|
||||
|
||||
contact = app.contacts.get_groupchat_contact(self._account,
|
||||
str(properties.jid))
|
||||
if contact is None:
|
||||
return
|
||||
|
||||
self._log.info('Captcha challenge received from %s', properties.jid)
|
||||
store_bob_data(properties.captcha.bob_data)
|
||||
muc_data.captcha_id = properties.id
|
||||
|
||||
self._manager.set_state(properties.jid, MUCJoinedState.CAPTCHA_REQUEST)
|
||||
self._remove_rejoin_timeout(properties.jid)
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('muc-captcha-challenge',
|
||||
account=self._account,
|
||||
room_jid=properties.jid.bare,
|
||||
form=properties.captcha.form))
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def cancel_captcha(self, room_jid):
|
||||
muc_data = self._manager.get(room_jid)
|
||||
if muc_data is None:
|
||||
return
|
||||
|
||||
if muc_data.captcha_id is None:
|
||||
self._log.warning('No captcha message id available')
|
||||
return
|
||||
self._nbxmpp('MUC').cancel_captcha(room_jid, muc_data.captcha_id)
|
||||
self._manager.set_state(room_jid, MUCJoinedState.CAPTCHA_FAILED)
|
||||
self._manager.set_state(room_jid, MUCJoinedState.NOT_JOINED)
|
||||
|
||||
def send_captcha(self, room_jid, form_node):
|
||||
self._manager.set_state(room_jid, MUCJoinedState.JOINING)
|
||||
self._nbxmpp('MUC').send_captcha(room_jid,
|
||||
form_node,
|
||||
callback=self._on_captcha_result)
|
||||
|
||||
def _on_captcha_result(self, task):
|
||||
try:
|
||||
task.finish()
|
||||
except StanzaError as error:
|
||||
muc_data = self._manager.get(error.jid)
|
||||
if muc_data is None:
|
||||
return
|
||||
self._manager.set_state(error.jid, MUCJoinedState.CAPTCHA_FAILED)
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('muc-captcha-error',
|
||||
account=self._account,
|
||||
room_jid=str(error.jid),
|
||||
error_text=to_user_string(error)))
|
||||
|
||||
def _on_config_change(self, _con, _stanza, properties):
|
||||
if not properties.is_muc_config_change:
|
||||
return
|
||||
|
||||
room_jid = str(properties.muc_jid)
|
||||
self._log.info('Received config change: %s %s',
|
||||
room_jid, properties.muc_status_codes)
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('muc-config-changed',
|
||||
account=self._account,
|
||||
room_jid=room_jid,
|
||||
status_codes=properties.muc_status_codes))
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _on_invite_or_decline(self, _con, _stanza, properties):
|
||||
if properties.muc_decline is not None:
|
||||
data = properties.muc_decline
|
||||
if helpers.ignore_contact(self._account, data.from_):
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
self._log.info('Invite declined from: %s, reason: %s',
|
||||
data.from_, data.reason)
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('muc-decline',
|
||||
account=self._account,
|
||||
**data._asdict()))
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
if properties.muc_invite is not None:
|
||||
data = properties.muc_invite
|
||||
if helpers.ignore_contact(self._account, data.from_):
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
self._log.info('Invite from: %s, to: %s', data.from_, data.muc)
|
||||
|
||||
if app.in_groupchat(self._account, data.muc):
|
||||
# We are already in groupchat. Ignore invitation
|
||||
self._log.info('We are already in this room')
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
self._con.get_module('Discovery').disco_muc(
|
||||
data.muc,
|
||||
request_vcard=True,
|
||||
callback=self._on_disco_result_after_invite,
|
||||
user_data=data)
|
||||
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _on_disco_result_after_invite(self, task):
|
||||
try:
|
||||
result = task.finish()
|
||||
except StanzaError as error:
|
||||
self._log.warning(error)
|
||||
return
|
||||
|
||||
invite_data = task.get_user_data()
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('muc-invitation',
|
||||
account=self._account,
|
||||
info=result.info,
|
||||
**invite_data._asdict()))
|
||||
|
||||
def invite(self, room, to, reason=None, continue_=False):
|
||||
type_ = InviteType.MEDIATED
|
||||
contact = app.contacts.get_contact_from_full_jid(self._account, to)
|
||||
if contact and contact.supports(Namespace.CONFERENCE):
|
||||
type_ = InviteType.DIRECT
|
||||
|
||||
password = app.gc_passwords.get(room, None)
|
||||
self._log.info('Invite %s to %s', to, room)
|
||||
return self._nbxmpp('MUC').invite(room, to, reason, password,
|
||||
continue_, type_)
|
||||
|
||||
@event_filter(['account'])
|
||||
def _on_account_disconnected(self, _event):
|
||||
for room_jid in list(self._rejoin_timeouts.keys()):
|
||||
self._remove_rejoin_timeout(room_jid)
|
||||
|
||||
for room_jid in list(self._join_timeouts.keys()):
|
||||
self._remove_join_timeout(room_jid)
|
||||
|
||||
|
||||
class MUCManager:
|
||||
def __init__(self, logger):
|
||||
self._log = logger
|
||||
self._mucs = {}
|
||||
|
||||
def add(self, muc):
|
||||
self._mucs[muc.jid] = muc
|
||||
|
||||
def remove(self, muc):
|
||||
self._mucs.pop(muc.jid, None)
|
||||
|
||||
def get(self, room_jid):
|
||||
return self._mucs.get(room_jid)
|
||||
|
||||
def set_state(self, room_jid, state):
|
||||
muc = self._mucs.get(room_jid)
|
||||
if muc is not None:
|
||||
if muc.state == state:
|
||||
return
|
||||
self._log.info('Set MUC state: %s %s', room_jid, state)
|
||||
muc.state = state
|
||||
|
||||
def get_joined_mucs(self):
|
||||
mucs = self._mucs.values()
|
||||
return [muc.jid for muc in mucs if muc.state == MUCJoinedState.JOINED]
|
||||
|
||||
def get_mucs_with_state(self, states):
|
||||
return [muc for muc in self._mucs.values() if muc.state in states]
|
||||
|
||||
def reset_state(self):
|
||||
for muc in self._mucs.values():
|
||||
self.set_state(muc.jid, MUCJoinedState.NOT_JOINED)
|
||||
|
||||
def __contains__(self, room_jid):
|
||||
return room_jid in self._mucs
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return MUC(*args, **kwargs), 'MUC'
|
39
gajim/common/modules/pep.py
Normal file
39
gajim/common/modules/pep.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
# 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-0163: Personal Eventing Protocol
|
||||
|
||||
from typing import Any
|
||||
from typing import Tuple
|
||||
|
||||
from gajim.common.types import ConnectionT
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class PEP(BaseModule):
|
||||
def __init__(self, con: ConnectionT) -> None:
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.supported = False
|
||||
|
||||
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
|
||||
|
||||
|
||||
def get_instance(*args: Any, **kwargs: Any) -> Tuple[PEP, str]:
|
||||
return PEP(*args, **kwargs), 'PEP'
|
82
gajim/common/modules/ping.py
Normal file
82
gajim/common/modules/ping.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
# 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-0199: XMPP Ping
|
||||
|
||||
from typing import Any
|
||||
from typing import Tuple
|
||||
from typing import Generator
|
||||
|
||||
import time
|
||||
|
||||
from nbxmpp.errors import is_error
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.types import ConnectionT
|
||||
from gajim.common.types import ContactsT
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.modules.util import as_task
|
||||
|
||||
|
||||
class Ping(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'Ping'
|
||||
_nbxmpp_methods = [
|
||||
'ping',
|
||||
]
|
||||
|
||||
def __init__(self, con: ConnectionT) -> None:
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = []
|
||||
|
||||
@as_task
|
||||
def send_ping(self, contact: ContactsT) -> Generator:
|
||||
_task = yield
|
||||
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
|
||||
jid = contact.get_full_jid()
|
||||
|
||||
self._log.info('Send ping to %s', jid)
|
||||
|
||||
app.nec.push_incoming_event(NetworkEvent('ping-sent',
|
||||
account=self._account,
|
||||
contact=contact))
|
||||
|
||||
ping_time = time.time()
|
||||
|
||||
response = yield self.ping(jid, timeout=10)
|
||||
if is_error(response):
|
||||
app.nec.push_incoming_event(NetworkEvent(
|
||||
'ping-error',
|
||||
account=self._account,
|
||||
contact=contact,
|
||||
error=str(response)))
|
||||
return
|
||||
|
||||
diff = round(time.time() - ping_time, 2)
|
||||
self._log.info('Received pong from %s after %s seconds',
|
||||
response.jid, diff)
|
||||
|
||||
app.nec.push_incoming_event(NetworkEvent('ping-reply',
|
||||
account=self._account,
|
||||
contact=contact,
|
||||
seconds=diff))
|
||||
|
||||
|
||||
def get_instance(*args: Any, **kwargs: Any) -> Tuple[Ping, str]:
|
||||
return Ping(*args, **kwargs), 'Ping'
|
394
gajim/common/modules/presence.py
Normal file
394
gajim/common/modules/presence.py
Normal file
|
@ -0,0 +1,394 @@
|
|||
# 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/>.
|
||||
|
||||
# Presence handler
|
||||
|
||||
import time
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
from nbxmpp.const import PresenceType
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common import idle
|
||||
from gajim.common.i18n import _
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.helpers import should_log
|
||||
from gajim.common.const import KindConstant
|
||||
from gajim.common.const import ShowConstant
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class Presence(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='presence',
|
||||
callback=self._presence_received,
|
||||
priority=50),
|
||||
StanzaHandler(name='presence',
|
||||
callback=self._subscribe_received,
|
||||
typ='subscribe',
|
||||
priority=49),
|
||||
StanzaHandler(name='presence',
|
||||
callback=self._subscribed_received,
|
||||
typ='subscribed',
|
||||
priority=49),
|
||||
StanzaHandler(name='presence',
|
||||
callback=self._unsubscribe_received,
|
||||
typ='unsubscribe',
|
||||
priority=49),
|
||||
StanzaHandler(name='presence',
|
||||
callback=self._unsubscribed_received,
|
||||
typ='unsubscribed',
|
||||
priority=49),
|
||||
]
|
||||
|
||||
# keep the jids we auto added (transports contacts) to not send the
|
||||
# SUBSCRIBED event to GUI
|
||||
self.automatically_added = []
|
||||
|
||||
# list of jid to auto-authorize
|
||||
self._jids_for_auto_auth = set()
|
||||
|
||||
def _presence_received(self, _con, stanza, properties):
|
||||
if properties.from_muc:
|
||||
# MUC occupant presences are already handled in MUC module
|
||||
return
|
||||
|
||||
muc = self._con.get_module('MUC').get_manager().get(properties.jid)
|
||||
if muc is not None:
|
||||
# Presence from the MUC itself, used for MUC avatar
|
||||
# handled in VCardAvatars module
|
||||
return
|
||||
|
||||
self._log.info('Received from %s', properties.jid)
|
||||
|
||||
if properties.type == PresenceType.ERROR:
|
||||
self._log.info('Error: %s %s', properties.jid, properties.error)
|
||||
return
|
||||
|
||||
if self._account == 'Local':
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('raw-pres-received',
|
||||
conn=self._con,
|
||||
stanza=stanza))
|
||||
return
|
||||
|
||||
if properties.is_self_presence:
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('our-show',
|
||||
account=self._account,
|
||||
show=properties.show.value))
|
||||
return
|
||||
|
||||
jid = properties.jid.bare
|
||||
roster_item = self._con.get_module('Roster').get_item(jid)
|
||||
if not properties.is_self_bare and roster_item is None:
|
||||
# Handle only presence from roster contacts
|
||||
self._log.warning('Unknown presence received')
|
||||
self._log.warning(stanza)
|
||||
return
|
||||
|
||||
show = properties.show.value
|
||||
if properties.type.is_unavailable:
|
||||
show = 'offline'
|
||||
|
||||
event_attrs = {
|
||||
'conn': self._con,
|
||||
'stanza': stanza,
|
||||
'prio': properties.priority,
|
||||
'need_add_in_roster': False,
|
||||
'popup': False,
|
||||
'ptype': properties.type.value,
|
||||
'jid': properties.jid.bare,
|
||||
'resource': properties.jid.resource,
|
||||
'id_': properties.id,
|
||||
'fjid': str(properties.jid),
|
||||
'timestamp': properties.timestamp,
|
||||
'avatar_sha': properties.avatar_sha,
|
||||
'user_nick': properties.nickname,
|
||||
'idle_time': properties.idle_timestamp,
|
||||
'show': show,
|
||||
'new_show': show,
|
||||
'old_show': 0,
|
||||
'status': properties.status,
|
||||
'contact_list': [],
|
||||
'contact': None,
|
||||
}
|
||||
|
||||
event_ = NetworkEvent('presence-received', **event_attrs)
|
||||
|
||||
# TODO: Refactor
|
||||
self._update_contact(event_, properties)
|
||||
|
||||
app.nec.push_incoming_event(event_)
|
||||
|
||||
def _update_contact(self, event, properties):
|
||||
# Note: A similar method also exists in connection_zeroconf
|
||||
jid = properties.jid.bare
|
||||
resource = properties.jid.resource
|
||||
|
||||
status_strings = ['offline', 'error', 'online', 'chat', 'away',
|
||||
'xa', 'dnd']
|
||||
|
||||
event.new_show = status_strings.index(event.show)
|
||||
|
||||
# Update contact
|
||||
contact_list = app.contacts.get_contacts(self._account, jid)
|
||||
if not contact_list:
|
||||
self._log.warning('No contact found')
|
||||
return
|
||||
|
||||
event.contact_list = contact_list
|
||||
|
||||
contact = app.contacts.get_contact_strict(self._account,
|
||||
properties.jid.bare,
|
||||
properties.jid.resource)
|
||||
if contact is None:
|
||||
contact = app.contacts.get_first_contact_from_jid(self._account,
|
||||
jid)
|
||||
if contact is None:
|
||||
self._log.warning('First contact not found')
|
||||
return
|
||||
|
||||
if (self._is_resource_known(contact_list) and
|
||||
not app.jid_is_transport(jid)):
|
||||
# Another resource of an existing contact connected
|
||||
# Add new contact
|
||||
event.old_show = 0
|
||||
contact = app.contacts.copy_contact(contact)
|
||||
contact.resource = resource
|
||||
app.contacts.add_contact(self._account, contact)
|
||||
else:
|
||||
# Convert the initial roster contact to a contact with resource
|
||||
contact.resource = resource
|
||||
event.old_show = 0
|
||||
if contact.show in status_strings:
|
||||
event.old_show = status_strings.index(contact.show)
|
||||
|
||||
event.need_add_in_roster = True
|
||||
|
||||
elif contact.show in status_strings:
|
||||
event.old_show = status_strings.index(contact.show)
|
||||
|
||||
# Update contact with presence data
|
||||
contact.show = event.show
|
||||
contact.status = properties.status
|
||||
contact.priority = properties.priority
|
||||
contact.idle_time = properties.idle_timestamp
|
||||
|
||||
event.contact = contact
|
||||
|
||||
if not app.jid_is_transport(jid) and len(contact_list) == 1:
|
||||
# It's not an agent
|
||||
if event.old_show == 0 and event.new_show > 1:
|
||||
if not jid in app.newly_added[self._account]:
|
||||
app.newly_added[self._account].append(jid)
|
||||
if jid in app.to_be_removed[self._account]:
|
||||
app.to_be_removed[self._account].remove(jid)
|
||||
elif event.old_show > 1 and event.new_show == 0 and \
|
||||
self._con.state.is_available:
|
||||
if not jid in app.to_be_removed[self._account]:
|
||||
app.to_be_removed[self._account].append(jid)
|
||||
if jid in app.newly_added[self._account]:
|
||||
app.newly_added[self._account].remove(jid)
|
||||
|
||||
if app.jid_is_transport(jid):
|
||||
return
|
||||
|
||||
if properties.type.is_unavailable:
|
||||
# TODO: This causes problems when another
|
||||
# resource signs off!
|
||||
self._con.get_module('Bytestream').stop_all_active_file_transfers(
|
||||
contact)
|
||||
self._log_presence(properties)
|
||||
|
||||
@staticmethod
|
||||
def _is_resource_known(contact_list):
|
||||
if len(contact_list) > 1:
|
||||
return True
|
||||
|
||||
if contact_list[0].resource == '':
|
||||
return False
|
||||
return contact_list[0].show not in ('not in roster', 'offline')
|
||||
|
||||
def _log_presence(self, properties):
|
||||
if not app.settings.get('log_contact_status_changes'):
|
||||
return
|
||||
if not should_log(self._account, properties.jid.bare):
|
||||
return
|
||||
|
||||
show = ShowConstant[properties.show.name]
|
||||
if properties.type.is_unavailable:
|
||||
show = ShowConstant.OFFLINE
|
||||
|
||||
app.storage.archive.insert_into_logs(self._account,
|
||||
properties.jid.bare,
|
||||
time.time(),
|
||||
KindConstant.STATUS,
|
||||
message=properties.status,
|
||||
show=show)
|
||||
|
||||
def _subscribe_received(self, _con, _stanza, properties):
|
||||
jid = properties.jid.bare
|
||||
fjid = str(properties.jid)
|
||||
|
||||
is_transport = app.jid_is_transport(fjid)
|
||||
auto_auth = app.settings.get_account_setting(self._account, 'autoauth')
|
||||
|
||||
self._log.info('Received Subscribe: %s, transport: %s, '
|
||||
'auto_auth: %s, user_nick: %s',
|
||||
properties.jid, is_transport,
|
||||
auto_auth, properties.nickname)
|
||||
|
||||
if auto_auth or jid in self._jids_for_auto_auth:
|
||||
self.send_presence(fjid, 'subscribed')
|
||||
self._jids_for_auto_auth.discard(jid)
|
||||
self._log.info('Auto respond with subscribed: %s', jid)
|
||||
return
|
||||
|
||||
status = (properties.status or
|
||||
_('I would like to add you to my roster.'))
|
||||
|
||||
app.nec.push_incoming_event(NetworkEvent(
|
||||
'subscribe-presence-received',
|
||||
conn=self._con,
|
||||
jid=jid,
|
||||
fjid=fjid,
|
||||
status=status,
|
||||
user_nick=properties.nickname,
|
||||
is_transport=is_transport))
|
||||
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _subscribed_received(self, _con, _stanza, properties):
|
||||
jid = properties.jid.bare
|
||||
self._log.info('Received Subscribed: %s', properties.jid)
|
||||
if jid in self.automatically_added:
|
||||
self.automatically_added.remove(jid)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
app.nec.push_incoming_event(NetworkEvent(
|
||||
'subscribed-presence-received',
|
||||
account=self._account,
|
||||
jid=properties.jid))
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _unsubscribe_received(self, _con, _stanza, properties):
|
||||
self._log.info('Received Unsubscribe: %s', properties.jid)
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _unsubscribed_received(self, _con, _stanza, properties):
|
||||
self._log.info('Received Unsubscribed: %s', properties.jid)
|
||||
app.nec.push_incoming_event(NetworkEvent(
|
||||
'unsubscribed-presence-received',
|
||||
conn=self._con, jid=properties.jid.bare))
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def subscribed(self, jid):
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
self._log.info('Subscribed: %s', jid)
|
||||
self.send_presence(jid, 'subscribed')
|
||||
|
||||
def unsubscribed(self, jid):
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
|
||||
self._log.info('Unsubscribed: %s', jid)
|
||||
self._jids_for_auto_auth.discard(jid)
|
||||
self.send_presence(jid, 'unsubscribed')
|
||||
|
||||
def unsubscribe(self, jid, remove_auth=True):
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
if remove_auth:
|
||||
self._con.get_module('Roster').del_item(jid)
|
||||
else:
|
||||
self._log.info('Unsubscribe from %s', jid)
|
||||
self._jids_for_auto_auth.discard(jid)
|
||||
self._con.get_module('Roster').unsubscribe(jid)
|
||||
self._con.get_module('Roster').set_item(jid)
|
||||
|
||||
def subscribe(self, jid, msg=None, name='', groups=None, auto_auth=False):
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
if groups is None:
|
||||
groups = []
|
||||
|
||||
self._log.info('Request Subscription to %s', jid)
|
||||
|
||||
if auto_auth:
|
||||
self._jids_for_auto_auth.add(jid)
|
||||
|
||||
infos = {'jid': jid}
|
||||
if name:
|
||||
infos['name'] = name
|
||||
iq = nbxmpp.Iq('set', Namespace.ROSTER)
|
||||
query = iq.setQuery()
|
||||
item = query.addChild('item', attrs=infos)
|
||||
for group in groups:
|
||||
item.addChild('group').setData(group)
|
||||
self._con.connection.send(iq)
|
||||
|
||||
self.send_presence(jid,
|
||||
'subscribe',
|
||||
status=msg,
|
||||
nick=app.nicks[self._account])
|
||||
|
||||
def get_presence(self, to=None, typ=None, priority=None,
|
||||
show=None, status=None, nick=None, caps=True,
|
||||
idle_time=False):
|
||||
if show not in ('chat', 'away', 'xa', 'dnd'):
|
||||
# Gajim sometimes passes invalid show values here
|
||||
# until this is fixed this is a workaround
|
||||
show = None
|
||||
presence = nbxmpp.Presence(to, typ, priority, show, status)
|
||||
if nick is not None:
|
||||
nick_tag = presence.setTag('nick', namespace=Namespace.NICK)
|
||||
nick_tag.setData(nick)
|
||||
|
||||
if (idle_time and
|
||||
app.is_installed('IDLE') and
|
||||
app.settings.get('autoaway')):
|
||||
idle_sec = idle.Monitor.get_idle_sec()
|
||||
time_ = time.strftime('%Y-%m-%dT%H:%M:%SZ',
|
||||
time.gmtime(time.time() - idle_sec))
|
||||
|
||||
idle_node = presence.setTag('idle', namespace=Namespace.IDLE)
|
||||
idle_node.setAttr('since', time_)
|
||||
|
||||
caps = self._con.get_module('Caps').caps
|
||||
if caps is not None and typ != 'unavailable':
|
||||
presence.setTag('c',
|
||||
namespace=Namespace.CAPS,
|
||||
attrs=caps._asdict())
|
||||
|
||||
return presence
|
||||
|
||||
def send_presence(self, *args, **kwargs):
|
||||
if not app.account_is_connected(self._account):
|
||||
return
|
||||
presence = self.get_presence(*args, **kwargs)
|
||||
app.plugin_manager.extension_point(
|
||||
'send-presence', self._account, presence)
|
||||
self._log.debug('Send presence:\n%s', presence)
|
||||
self._con.connection.send(presence)
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return Presence(*args, **kwargs), 'Presence'
|
85
gajim/common/modules/pubsub.py
Normal file
85
gajim/common/modules/pubsub.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
# Copyright (C) 2006 Tomasz Melcer <liori AT exroot.org>
|
||||
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
|
||||
# Copyright (C) 2007 Jean-Marie Traissard <jim AT lapin.org>
|
||||
# Copyright (C) 2008 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-0060: Publish-Subscribe
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class PubSub(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'PubSub'
|
||||
_nbxmpp_methods = [
|
||||
'publish',
|
||||
'delete',
|
||||
'set_node_configuration',
|
||||
'get_node_configuration',
|
||||
'get_access_model',
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.publish_options = False
|
||||
|
||||
def pass_disco(self, info):
|
||||
if Namespace.PUBSUB_PUBLISH_OPTIONS in info.features:
|
||||
self._log.info('Discovered Pubsub publish options: %s', info.jid)
|
||||
self.publish_options = True
|
||||
|
||||
def send_pb_subscription_query(self, jid, cb, **kwargs):
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
|
||||
query = nbxmpp.Iq('get', to=jid)
|
||||
pb = query.addChild('pubsub', namespace=Namespace.PUBSUB)
|
||||
pb.addChild('subscriptions')
|
||||
|
||||
self._con.connection.SendAndCallForResponse(query, cb, kwargs)
|
||||
|
||||
def send_pb_subscribe(self, jid, node, cb, **kwargs):
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
|
||||
our_jid = app.get_jid_from_account(self._account)
|
||||
query = nbxmpp.Iq('set', to=jid)
|
||||
pb = query.addChild('pubsub', namespace=Namespace.PUBSUB)
|
||||
pb.addChild('subscribe', {'node': node, 'jid': our_jid})
|
||||
|
||||
self._con.connection.SendAndCallForResponse(query, cb, kwargs)
|
||||
|
||||
def send_pb_unsubscribe(self, jid, node, cb, **kwargs):
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
|
||||
our_jid = app.get_jid_from_account(self._account)
|
||||
query = nbxmpp.Iq('set', to=jid)
|
||||
pb = query.addChild('pubsub', namespace=Namespace.PUBSUB)
|
||||
pb.addChild('unsubscribe', {'node': node, 'jid': our_jid})
|
||||
|
||||
self._con.connection.SendAndCallForResponse(query, cb, kwargs)
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return PubSub(*args, **kwargs), 'PubSub'
|
112
gajim/common/modules/receipts.py
Normal file
112
gajim/common/modules/receipts.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
# 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-0184: Message Delivery Receipts
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.modules.receipts import build_receipt
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class Receipts(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='message',
|
||||
callback=self._process_message_receipt,
|
||||
ns=Namespace.RECEIPTS,
|
||||
priority=46),
|
||||
]
|
||||
|
||||
def _process_message_receipt(self, _con, stanza, properties):
|
||||
if not properties.is_receipt:
|
||||
return
|
||||
|
||||
if properties.type.is_error:
|
||||
if properties.receipt.is_request:
|
||||
return
|
||||
# Don't propagate this event further
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
if (properties.type.is_groupchat or
|
||||
properties.is_self_message or
|
||||
properties.is_mam_message or
|
||||
properties.is_carbon_message and properties.carbon.is_sent):
|
||||
|
||||
if properties.receipt.is_received:
|
||||
# Don't propagate this event further
|
||||
raise nbxmpp.NodeProcessed
|
||||
return
|
||||
|
||||
if properties.receipt.is_request:
|
||||
if not app.settings.get_account_setting(self._account,
|
||||
'answer_receipts'):
|
||||
return
|
||||
|
||||
if properties.eme is not None:
|
||||
# Don't send receipt for message which couldn't be decrypted
|
||||
if not properties.is_encrypted:
|
||||
return
|
||||
|
||||
contact = self._get_contact(properties)
|
||||
if contact is None:
|
||||
return
|
||||
self._log.info('Send receipt: %s', properties.jid)
|
||||
self._con.connection.send(build_receipt(stanza))
|
||||
return
|
||||
|
||||
if properties.receipt.is_received:
|
||||
self._log.info('Receipt from %s %s',
|
||||
properties.jid,
|
||||
properties.receipt.id)
|
||||
|
||||
jid = properties.jid
|
||||
if not properties.is_muc_pm:
|
||||
jid = jid.new_as_bare()
|
||||
|
||||
app.storage.archive.set_marker(
|
||||
app.get_jid_from_account(self._account),
|
||||
jid,
|
||||
properties.receipt.id,
|
||||
'received')
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('receipt-received',
|
||||
account=self._account,
|
||||
jid=jid,
|
||||
receipt_id=properties.receipt.id))
|
||||
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _get_contact(self, properties):
|
||||
if properties.is_muc_pm:
|
||||
return app.contacts.get_gc_contact(self._account,
|
||||
properties.jid.bare,
|
||||
properties.jid.resource)
|
||||
|
||||
contact = app.contacts.get_contact(self._account,
|
||||
properties.jid.bare)
|
||||
if contact is not None and contact.sub not in ('to', 'none'):
|
||||
return contact
|
||||
return None
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return Receipts(*args, **kwargs), 'Receipts'
|
44
gajim/common/modules/register.py
Normal file
44
gajim/common/modules/register.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
# 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-0077: In-Band Registration
|
||||
|
||||
|
||||
from nbxmpp.namespaces import Namespace
|
||||
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class Register(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'Register'
|
||||
_nbxmpp_methods = [
|
||||
'unregister',
|
||||
'change_password',
|
||||
'change_password_with_form',
|
||||
'request_register_form',
|
||||
'submit_register_form',
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.supported = False
|
||||
|
||||
def pass_disco(self, info):
|
||||
self.supported = Namespace.REGISTER in info.features
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return Register(*args, **kwargs), 'Register'
|
379
gajim/common/modules/roster.py
Normal file
379
gajim/common/modules/roster.py
Normal file
|
@ -0,0 +1,379 @@
|
|||
# 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/>.
|
||||
|
||||
# Roster
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
RosterItem = namedtuple('RosterItem', 'jid data')
|
||||
|
||||
|
||||
class Roster(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='iq',
|
||||
callback=self._roster_push_received,
|
||||
typ='set',
|
||||
ns=Namespace.ROSTER),
|
||||
StanzaHandler(name='presence',
|
||||
callback=self._presence_received),
|
||||
]
|
||||
|
||||
self._data = {}
|
||||
self._set = None
|
||||
|
||||
def load_roster(self):
|
||||
self._log.info('Load from database')
|
||||
data = app.storage.cache.load_roster(self._account)
|
||||
if data:
|
||||
self.set_raw(data)
|
||||
for jid, item in self._data.items():
|
||||
self._log.debug('%s: %s', jid, item)
|
||||
app.nec.push_incoming_event(NetworkEvent(
|
||||
'roster-info',
|
||||
conn=self._con,
|
||||
jid=jid,
|
||||
nickname=item['name'],
|
||||
sub=item['subscription'],
|
||||
ask=item['ask'],
|
||||
groups=item['groups'],
|
||||
avatar_sha=item.get('avatar_sha')))
|
||||
else:
|
||||
self._log.info('Database empty, reset roster version')
|
||||
app.settings.set_account_setting(
|
||||
self._account, 'roster_version', '')
|
||||
|
||||
app.nec.push_incoming_event(NetworkEvent(
|
||||
'roster-received',
|
||||
conn=self._con,
|
||||
roster=self._data.copy(),
|
||||
received_from_server=False))
|
||||
|
||||
def _store_roster(self):
|
||||
app.storage.cache.store_roster(self._account, self._data)
|
||||
|
||||
def request_roster(self):
|
||||
version = None
|
||||
features = self._con.connection.features
|
||||
if features.has_roster_version:
|
||||
version = app.settings.get_account_setting(self._account,
|
||||
'roster_version')
|
||||
|
||||
self._log.info('Requested from server')
|
||||
iq = nbxmpp.Iq('get', Namespace.ROSTER)
|
||||
if version is not None:
|
||||
iq.setTagAttr('query', 'ver', version)
|
||||
self._log.info('Request version: %s', version)
|
||||
self._con.connection.SendAndCallForResponse(
|
||||
iq, self._roster_received)
|
||||
|
||||
def _roster_received(self, _nbxmpp_client, stanza):
|
||||
if not nbxmpp.isResultNode(stanza):
|
||||
self._log.warning('Unable to retrieve roster: %s',
|
||||
stanza.getError())
|
||||
else:
|
||||
self._log.info('Received Roster')
|
||||
received_from_server = False
|
||||
if stanza.getTag('query') is not None:
|
||||
# clear Roster
|
||||
self._data = {}
|
||||
version = self._parse_roster(stanza)
|
||||
|
||||
self._log.info('New version: %s', version)
|
||||
self._store_roster()
|
||||
app.settings.set_account_setting(self._account,
|
||||
'roster_version',
|
||||
version)
|
||||
|
||||
received_from_server = True
|
||||
|
||||
app.nec.push_incoming_event(NetworkEvent(
|
||||
'roster-received',
|
||||
conn=self._con,
|
||||
roster=self._data.copy(),
|
||||
received_from_server=received_from_server))
|
||||
|
||||
self._con.connect_machine()
|
||||
|
||||
def _roster_push_received(self, _con, stanza, _properties):
|
||||
self._log.info('Push received')
|
||||
|
||||
sender = stanza.getFrom()
|
||||
if sender is not None:
|
||||
if not self._con.get_own_jid().bare_match(sender):
|
||||
self._log.warning('Wrong JID %s', stanza.getFrom())
|
||||
return
|
||||
|
||||
push_items, version = self._parse_push(stanza)
|
||||
|
||||
self._ack_roster_push(stanza)
|
||||
|
||||
for item in push_items:
|
||||
attrs = item.data
|
||||
app.nec.push_incoming_event(NetworkEvent(
|
||||
'roster-info',
|
||||
conn=self._con,
|
||||
jid=item.jid,
|
||||
nickname=attrs['name'],
|
||||
sub=attrs['subscription'],
|
||||
ask=attrs['ask'],
|
||||
groups=attrs['groups'],
|
||||
avatar_sha=None))
|
||||
|
||||
self._store_roster()
|
||||
|
||||
self._log.info('New version: %s', version)
|
||||
app.settings.set_account_setting(self._account,
|
||||
'roster_version',
|
||||
version)
|
||||
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def _parse_roster(self, stanza):
|
||||
query = stanza.getTag('query')
|
||||
version = query.getAttr('ver')
|
||||
|
||||
for item in query.getTags('item'):
|
||||
jid = item.getAttr('jid')
|
||||
self._data[jid] = self._get_item_attrs(item)
|
||||
self._log.info('Item %s: %s', jid, self._data[jid])
|
||||
return version
|
||||
|
||||
@staticmethod
|
||||
def _get_item_attrs(item, update=False):
|
||||
'''
|
||||
update: True
|
||||
Omit avatar_sha from the returned attrs
|
||||
|
||||
update: False
|
||||
Include the default value from avatar_sha in the returned attrs
|
||||
'''
|
||||
|
||||
default_attrs = {'name': None,
|
||||
'ask': None,
|
||||
'subscription': None,
|
||||
'groups': []}
|
||||
|
||||
if not update:
|
||||
default_attrs['avatar_sha'] = None
|
||||
|
||||
attrs = item.getAttrs()
|
||||
del attrs['jid']
|
||||
groups = {group.getData() for group in item.getTags('group')}
|
||||
attrs['groups'] = list(groups)
|
||||
|
||||
default_attrs.update(attrs)
|
||||
return default_attrs
|
||||
|
||||
def _parse_push(self, stanza):
|
||||
query = stanza.getTag('query')
|
||||
version = query.getAttr('ver')
|
||||
push_items = []
|
||||
|
||||
for item in query.getTags('item'):
|
||||
push_items.append(self._update_roster_item(item))
|
||||
for item in push_items:
|
||||
self._log.info('Push: %s', item)
|
||||
return push_items, version
|
||||
|
||||
def _update_roster_item(self, item):
|
||||
jid = item.getAttr('jid')
|
||||
|
||||
if item.getAttr('subscription') == 'remove':
|
||||
self._data.pop(jid, None)
|
||||
attrs = self._get_item_attrs(item)
|
||||
return RosterItem(jid, attrs)
|
||||
|
||||
if jid not in self._data:
|
||||
self._data[jid] = self._get_item_attrs(item)
|
||||
else:
|
||||
self._data[jid].update(self._get_item_attrs(item, update=True))
|
||||
|
||||
return RosterItem(jid, self._data[jid])
|
||||
|
||||
def _ack_roster_push(self, stanza):
|
||||
iq = nbxmpp.Iq('result',
|
||||
to=stanza.getFrom(),
|
||||
frm=stanza.getTo(),
|
||||
attrs={'id': stanza.getID()})
|
||||
self._con.connection.send(iq)
|
||||
|
||||
def _presence_received(self, _con, pres, _properties):
|
||||
'''
|
||||
Add contacts that request subscription to our internal
|
||||
roster and also to the database. The contact is put into the
|
||||
'Not in contact list' group and because we save it to the database
|
||||
it is also after a restart available.
|
||||
'''
|
||||
|
||||
if pres.getType() != 'subscribe':
|
||||
return
|
||||
|
||||
jid = pres.getFrom().bare
|
||||
|
||||
if jid in self._data:
|
||||
return
|
||||
|
||||
self._log.info('Add Contact from presence %s', jid)
|
||||
self._data[jid] = {'name': None,
|
||||
'ask': None,
|
||||
'subscription':
|
||||
'none',
|
||||
'groups': ['Not in contact list']}
|
||||
|
||||
self._store_roster()
|
||||
|
||||
def _get_item_data(self, jid, dataname):
|
||||
"""
|
||||
Return specific jid's representation in internal format.
|
||||
"""
|
||||
jid = jid[:(jid + '/').find('/')]
|
||||
return self._data[jid][dataname]
|
||||
|
||||
def del_item(self, jid):
|
||||
"""
|
||||
Delete contact 'jid' from roster
|
||||
"""
|
||||
self._con.connection.send(
|
||||
nbxmpp.Iq('set', Namespace.ROSTER, payload=[
|
||||
nbxmpp.Node('item', {'jid': jid, 'subscription': 'remove'})]))
|
||||
|
||||
def get_groups(self, jid):
|
||||
"""
|
||||
Return groups list that contact 'jid' belongs to
|
||||
"""
|
||||
return self._get_item_data(jid, 'groups')
|
||||
|
||||
def get_name(self, jid):
|
||||
"""
|
||||
Return name of contact 'jid'
|
||||
"""
|
||||
return self._get_item_data(jid, 'name')
|
||||
|
||||
def update_contact(self, jid, name, groups):
|
||||
if app.account_is_available(self._account):
|
||||
self.set_item(jid=jid, name=name, groups=groups)
|
||||
|
||||
def update_contacts(self, contacts):
|
||||
"""
|
||||
Update multiple roster items
|
||||
"""
|
||||
if app.account_is_available(self._account):
|
||||
self.set_item_multi(contacts)
|
||||
|
||||
def set_item(self, jid, name=None, groups=None):
|
||||
"""
|
||||
Rename contact 'jid' and sets the groups list that it now belongs to
|
||||
"""
|
||||
iq = nbxmpp.Iq('set', Namespace.ROSTER)
|
||||
query = iq.getTag('query')
|
||||
attrs = {'jid': jid}
|
||||
if name:
|
||||
attrs['name'] = name
|
||||
item = query.setTag('item', attrs)
|
||||
if groups is not None:
|
||||
for group in groups:
|
||||
item.addChild(node=nbxmpp.Node('group', payload=[group]))
|
||||
self._con.connection.send(iq)
|
||||
|
||||
def set_item_multi(self, items):
|
||||
"""
|
||||
Rename multiple contacts and sets their group lists
|
||||
"""
|
||||
for i in items:
|
||||
iq = nbxmpp.Iq('set', Namespace.ROSTER)
|
||||
query = iq.getTag('query')
|
||||
attrs = {'jid': i['jid']}
|
||||
if i['name']:
|
||||
attrs['name'] = i['name']
|
||||
item = query.setTag('item', attrs)
|
||||
for group in i['groups']:
|
||||
item.addChild(node=nbxmpp.Node('group', payload=[group]))
|
||||
self._con.connection.send(iq)
|
||||
|
||||
def get_items(self):
|
||||
"""
|
||||
Return list of all [bare] JIDs that the roster is currently tracks
|
||||
"""
|
||||
return list(self._data.keys())
|
||||
|
||||
def keys(self):
|
||||
"""
|
||||
Same as get_items. Provided for the sake of dictionary interface
|
||||
"""
|
||||
return list(self._data.keys())
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""
|
||||
Get the contact in the internal format.
|
||||
Raises KeyError if JID 'item' is not in roster
|
||||
"""
|
||||
return self._data[item]
|
||||
|
||||
def get_item(self, item):
|
||||
"""
|
||||
Get the contact in the internal format (or None if JID 'item' is not in
|
||||
roster)
|
||||
"""
|
||||
if item in self._data:
|
||||
return self._data[item]
|
||||
return None
|
||||
|
||||
def unsubscribe(self, jid):
|
||||
"""
|
||||
Ask for removing our subscription for JID 'jid'
|
||||
"""
|
||||
self._con.connection.send(nbxmpp.Presence(jid, 'unsubscribe'))
|
||||
|
||||
def get_raw(self):
|
||||
"""
|
||||
Return the internal data representation of the roster
|
||||
"""
|
||||
return self._data
|
||||
|
||||
def set_raw(self, data):
|
||||
"""
|
||||
Set the internal data representation of the roster
|
||||
"""
|
||||
own_jid = self._con.get_own_jid().bare
|
||||
self._data = data
|
||||
self._data[own_jid] = {
|
||||
'resources': {},
|
||||
'name': None,
|
||||
'ask': None,
|
||||
'subscription': None,
|
||||
'groups': None,
|
||||
'avatar_sha': None
|
||||
}
|
||||
|
||||
def set_avatar_sha(self, jid, sha):
|
||||
if jid not in self._data:
|
||||
return
|
||||
|
||||
self._data[jid]['avatar_sha'] = sha
|
||||
self._store_roster()
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return Roster(*args, **kwargs), 'Roster'
|
130
gajim/common/modules/roster_item_exchange.py
Normal file
130
gajim/common/modules/roster_item_exchange.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
# 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-0144: Roster Item Exchange
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common import helpers
|
||||
from gajim.common.i18n import _
|
||||
from gajim.common.nec import NetworkIncomingEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class RosterItemExchange(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='iq',
|
||||
callback=self.received_item,
|
||||
typ='set',
|
||||
ns=Namespace.ROSTERX),
|
||||
StanzaHandler(name='message',
|
||||
callback=self.received_item,
|
||||
ns=Namespace.ROSTERX),
|
||||
]
|
||||
|
||||
def received_item(self, _con, stanza, _properties):
|
||||
# stanza can be a message or a iq
|
||||
|
||||
self._log.info('Received roster items from %s', stanza.getFrom())
|
||||
|
||||
exchange_items_list = {}
|
||||
items_list = stanza.getTag(
|
||||
'x', namespace=Namespace.ROSTERX).getChildren()
|
||||
if items_list is None:
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
action = items_list[0].getAttr('action')
|
||||
if not action:
|
||||
action = 'add'
|
||||
|
||||
for item in items_list:
|
||||
try:
|
||||
jid = helpers.parse_jid(item.getAttr('jid'))
|
||||
except helpers.InvalidFormat:
|
||||
self._log.warning('Invalid JID: %s, ignoring it',
|
||||
item.getAttr('jid'))
|
||||
continue
|
||||
name = item.getAttr('name')
|
||||
contact = app.contacts.get_contact(self._account, jid)
|
||||
groups = []
|
||||
same_groups = True
|
||||
for group in item.getTags('group'):
|
||||
groups.append(group.getData())
|
||||
# check that all suggested groups are in the groups we have for
|
||||
# this contact
|
||||
if not contact or group not in contact.groups:
|
||||
same_groups = False
|
||||
if contact:
|
||||
# check that all groups we have for this contact are in the
|
||||
# suggested groups
|
||||
for group in contact.groups:
|
||||
if group not in groups:
|
||||
same_groups = False
|
||||
if contact.sub in ('both', 'to') and same_groups:
|
||||
continue
|
||||
exchange_items_list[jid] = [name, groups]
|
||||
|
||||
if not exchange_items_list:
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
self._log.info('Items: %s', exchange_items_list)
|
||||
|
||||
app.nec.push_incoming_event(RosterItemExchangeEvent(
|
||||
None, conn=self._con,
|
||||
fjid=str(stanza.getFrom()),
|
||||
exchange_items_list=exchange_items_list,
|
||||
action=action))
|
||||
|
||||
raise nbxmpp.NodeProcessed
|
||||
|
||||
def send_contacts(self, contacts, fjid, type_='message'):
|
||||
if not app.account_is_available(self._account):
|
||||
return
|
||||
|
||||
if type_ == 'message':
|
||||
if len(contacts) == 1:
|
||||
msg = _('Sent contact: "%(jid)s" (%(name)s)') % {
|
||||
'jid': contacts[0].get_full_jid(),
|
||||
'name': contacts[0].get_shown_name()}
|
||||
else:
|
||||
msg = _('Sent contacts:')
|
||||
for contact in contacts:
|
||||
msg += '\n "%s" (%s)' % (contact.get_full_jid(),
|
||||
contact.get_shown_name())
|
||||
stanza = nbxmpp.Message(to=app.get_jid_without_resource(fjid),
|
||||
body=msg)
|
||||
elif type_ == 'iq':
|
||||
stanza = nbxmpp.Iq(to=fjid, typ='set')
|
||||
xdata = stanza.addChild(name='x', namespace=Namespace.ROSTERX)
|
||||
for contact in contacts:
|
||||
name = contact.get_shown_name()
|
||||
xdata.addChild(name='item', attrs={'action': 'add',
|
||||
'jid': contact.jid,
|
||||
'name': name})
|
||||
self._log.info('Send contact: %s %s', contact.jid, name)
|
||||
self._con.connection.send(stanza)
|
||||
|
||||
|
||||
class RosterItemExchangeEvent(NetworkIncomingEvent):
|
||||
name = 'roster-item-exchange-received'
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return RosterItemExchange(*args, **kwargs), 'RosterItemExchange'
|
111
gajim/common/modules/search.py
Normal file
111
gajim/common/modules/search.py
Normal file
|
@ -0,0 +1,111 @@
|
|||
# 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-0055: Jabber Search
|
||||
|
||||
import nbxmpp
|
||||
from nbxmpp.namespaces import Namespace
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkIncomingEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class Search(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
def request_search_fields(self, jid):
|
||||
self._log.info('Request search fields from %s', jid)
|
||||
iq = nbxmpp.Iq(typ='get', to=jid, queryNS=Namespace.SEARCH)
|
||||
self._con.connection.SendAndCallForResponse(iq, self._fields_received)
|
||||
|
||||
def _fields_received(self, _nbxmpp_client, stanza):
|
||||
data = None
|
||||
is_dataform = False
|
||||
|
||||
if nbxmpp.isResultNode(stanza):
|
||||
self._log.info('Received search fields from %s', stanza.getFrom())
|
||||
tag = stanza.getTag('query', namespace=Namespace.SEARCH)
|
||||
if tag is None:
|
||||
self._log.info('Invalid stanza: %s', stanza)
|
||||
return
|
||||
|
||||
data = tag.getTag('x', namespace=Namespace.DATA)
|
||||
if data is not None:
|
||||
is_dataform = True
|
||||
else:
|
||||
data = {}
|
||||
for i in stanza.getQueryPayload():
|
||||
data[i.getName()] = i.getData()
|
||||
else:
|
||||
self._log.info('Error: %s', stanza.getError())
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
SearchFormReceivedEvent(None, conn=self._con,
|
||||
is_dataform=is_dataform,
|
||||
data=data))
|
||||
|
||||
def send_search_form(self, jid, form, is_form):
|
||||
iq = nbxmpp.Iq(typ='set', to=jid, queryNS=Namespace.SEARCH)
|
||||
item = iq.setQuery()
|
||||
if is_form:
|
||||
item.addChild(node=form)
|
||||
else:
|
||||
for i in form.keys():
|
||||
item.setTagData(i, form[i])
|
||||
|
||||
self._con.connection.SendAndCallForResponse(iq, self._received_result)
|
||||
|
||||
def _received_result(self, _nbxmpp_client, stanza):
|
||||
data = None
|
||||
is_dataform = False
|
||||
|
||||
if nbxmpp.isResultNode(stanza):
|
||||
self._log.info('Received result from %s', stanza.getFrom())
|
||||
tag = stanza.getTag('query', namespace=Namespace.SEARCH)
|
||||
if tag is None:
|
||||
self._log.info('Invalid stanza: %s', stanza)
|
||||
return
|
||||
|
||||
data = tag.getTag('x', namespace=Namespace.DATA)
|
||||
if data is not None:
|
||||
is_dataform = True
|
||||
else:
|
||||
data = []
|
||||
for item in tag.getTags('item'):
|
||||
# We also show attributes. jid is there
|
||||
field = item.attrs
|
||||
for i in item.getPayload():
|
||||
field[i.getName()] = i.getData()
|
||||
data.append(field)
|
||||
else:
|
||||
self._log.info('Error: %s', stanza.getError())
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
SearchResultReceivedEvent(None, conn=self._con,
|
||||
is_dataform=is_dataform,
|
||||
data=data))
|
||||
|
||||
|
||||
class SearchFormReceivedEvent(NetworkIncomingEvent):
|
||||
name = 'search-form-received'
|
||||
|
||||
|
||||
class SearchResultReceivedEvent(NetworkIncomingEvent):
|
||||
name = 'search-result-received'
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return Search(*args, **kwargs), 'Search'
|
70
gajim/common/modules/security_labels.py
Normal file
70
gajim/common/modules/security_labels.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
# 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-0258: Security Labels in XMPP
|
||||
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.errors import is_error
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.modules.util import as_task
|
||||
|
||||
|
||||
class SecLabels(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'SecurityLabels'
|
||||
_nbxmpp_methods = [
|
||||
'request_catalog',
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self._catalogs = {}
|
||||
self.supported = False
|
||||
|
||||
def pass_disco(self, info):
|
||||
if Namespace.SECLABEL not in info.features:
|
||||
return
|
||||
|
||||
self.supported = True
|
||||
self._log.info('Discovered security labels: %s', info.jid)
|
||||
|
||||
@as_task
|
||||
def request_catalog(self, jid):
|
||||
_task = yield
|
||||
|
||||
catalog = yield self._nbxmpp('SecurityLabels').request_catalog(jid)
|
||||
|
||||
if is_error(catalog):
|
||||
self._log.info(catalog)
|
||||
return
|
||||
|
||||
self._catalogs[jid] = catalog
|
||||
|
||||
self._log.info('Received catalog: %s', jid)
|
||||
|
||||
app.nec.push_incoming_event(NetworkEvent('sec-catalog-received',
|
||||
account=self._account,
|
||||
jid=jid,
|
||||
catalog=catalog))
|
||||
|
||||
def get_catalog(self, jid):
|
||||
return self._catalogs.get(jid)
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return SecLabels(*args, **kwargs), 'SecLabels'
|
46
gajim/common/modules/software_version.py
Normal file
46
gajim/common/modules/software_version.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
# 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-0092: Software Version
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.helpers import get_os_info
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class SoftwareVersion(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'SoftwareVersion'
|
||||
_nbxmpp_methods = [
|
||||
'set_software_version',
|
||||
'request_software_version',
|
||||
'disable',
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
if enabled:
|
||||
if not app.settings.get_account_setting(self._account,
|
||||
'send_os_info'):
|
||||
return
|
||||
self._nbxmpp('SoftwareVersion').set_software_version(
|
||||
'Gajim', app.version, get_os_info())
|
||||
else:
|
||||
self._nbxmpp('SoftwareVersion').disable()
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return SoftwareVersion(*args, **kwargs), 'SoftwareVersion'
|
91
gajim/common/modules/user_activity.py
Normal file
91
gajim/common/modules/user_activity.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
# 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-0108: User Activity
|
||||
|
||||
from typing import Any
|
||||
from typing import Tuple
|
||||
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.structs import ActivityData
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.modules.util import event_node
|
||||
from gajim.common.const import PEPEventType
|
||||
|
||||
|
||||
class UserActivity(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'Activity'
|
||||
_nbxmpp_methods = [
|
||||
'set_activity',
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
self._register_pubsub_handler(self._activity_received)
|
||||
|
||||
self._current_activity = None
|
||||
|
||||
def get_current_activity(self):
|
||||
return self._current_activity
|
||||
|
||||
@event_node(Namespace.ACTIVITY)
|
||||
def _activity_received(self, _con, _stanza, properties):
|
||||
if properties.pubsub_event.retracted:
|
||||
return
|
||||
|
||||
data = properties.pubsub_event.data
|
||||
for contact in app.contacts.get_contacts(self._account,
|
||||
str(properties.jid)):
|
||||
if data is not None:
|
||||
contact.pep[PEPEventType.ACTIVITY] = data
|
||||
else:
|
||||
contact.pep.pop(PEPEventType.ACTIVITY, None)
|
||||
|
||||
if properties.is_self_message:
|
||||
if data is not None:
|
||||
self._con.pep[PEPEventType.ACTIVITY] = data
|
||||
else:
|
||||
self._con.pep.pop(PEPEventType.ACTIVITY, None)
|
||||
self._current_activity = data
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('activity-received',
|
||||
account=self._account,
|
||||
jid=properties.jid.bare,
|
||||
activity=data,
|
||||
is_self_message=properties.is_self_message))
|
||||
|
||||
def set_activity(self, activity):
|
||||
if activity is not None:
|
||||
activity = ActivityData(*activity, None)
|
||||
|
||||
if activity == self._current_activity:
|
||||
return
|
||||
|
||||
self._current_activity = activity
|
||||
|
||||
if activity is None:
|
||||
self._log.info('Remove user activity')
|
||||
else:
|
||||
self._log.info('Set %s', activity)
|
||||
|
||||
self._nbxmpp('Activity').set_activity(activity)
|
||||
|
||||
|
||||
def get_instance(*args: Any, **kwargs: Any) -> Tuple[UserActivity, str]:
|
||||
return UserActivity(*args, **kwargs), 'UserActivity'
|
99
gajim/common/modules/user_avatar.py
Normal file
99
gajim/common/modules/user_avatar.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
# 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-0084: User Avatar
|
||||
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.modules.util import is_error
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.modules.util import event_node
|
||||
from gajim.common.modules.util import as_task
|
||||
|
||||
class UserAvatar(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'UserAvatar'
|
||||
_nbxmpp_methods = [
|
||||
'request_avatar_metadata',
|
||||
'request_avatar_data',
|
||||
'set_avatar',
|
||||
'set_access_model'
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
self._register_pubsub_handler(self._avatar_metadata_received)
|
||||
|
||||
@event_node(Namespace.AVATAR_METADATA)
|
||||
def _avatar_metadata_received(self, _con, _stanza, properties):
|
||||
if properties.pubsub_event.retracted:
|
||||
return
|
||||
|
||||
metadata = properties.pubsub_event.data
|
||||
jid = str(properties.jid)
|
||||
|
||||
if metadata is None or not metadata.infos:
|
||||
self._log.info('No avatar published: %s', jid)
|
||||
app.contacts.set_avatar(self._account, jid, None)
|
||||
self._con.get_module('Roster').set_avatar_sha(jid, None)
|
||||
app.interface.update_avatar(self._account, jid)
|
||||
else:
|
||||
if properties.is_self_message:
|
||||
sha = app.settings.get_account_setting(self._account,
|
||||
'avatar_sha')
|
||||
else:
|
||||
sha = app.contacts.get_avatar_sha(self._account, jid)
|
||||
|
||||
if sha in metadata.avatar_shas:
|
||||
self._log.info('Avatar already known: %s %s', jid, sha)
|
||||
return
|
||||
|
||||
avatar_info = metadata.infos[0]
|
||||
self._log.info('Request: %s %s', jid, avatar_info.id)
|
||||
self._request_avatar_data(jid, avatar_info)
|
||||
|
||||
@as_task
|
||||
def _request_avatar_data(self, jid, avatar_info):
|
||||
_task = yield
|
||||
|
||||
avatar = yield self._nbxmpp('UserAvatar').request_avatar_data(
|
||||
avatar_info.id, jid=jid)
|
||||
|
||||
if avatar is None:
|
||||
self._log.warning('%s advertised %s but data node is empty',
|
||||
jid, avatar_info.id)
|
||||
return
|
||||
|
||||
if is_error(avatar):
|
||||
self._log.warning(avatar)
|
||||
return
|
||||
|
||||
self._log.info('Received Avatar: %s %s', jid, avatar.sha)
|
||||
app.interface.save_avatar(avatar.data)
|
||||
|
||||
if self._con.get_own_jid().bare_match(jid):
|
||||
app.settings.set_account_setting(self._account,
|
||||
'avatar_sha',
|
||||
avatar.sha)
|
||||
else:
|
||||
self._con.get_module('Roster').set_avatar_sha(
|
||||
str(jid), avatar.sha)
|
||||
|
||||
app.contacts.set_avatar(self._account, str(jid), avatar.sha)
|
||||
app.interface.update_avatar(self._account, str(jid))
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return UserAvatar(*args, **kwargs), 'UserAvatar'
|
78
gajim/common/modules/user_location.py
Normal file
78
gajim/common/modules/user_location.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
# 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-0080: User Location
|
||||
|
||||
from nbxmpp.namespaces import Namespace
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.modules.util import event_node
|
||||
from gajim.common.modules.util import store_publish
|
||||
from gajim.common.const import PEPEventType
|
||||
|
||||
|
||||
class UserLocation(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'Location'
|
||||
_nbxmpp_methods = [
|
||||
'set_location',
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
self._register_pubsub_handler(self._location_received)
|
||||
|
||||
self._current_location = None
|
||||
|
||||
def get_current_location(self):
|
||||
return self._current_location
|
||||
|
||||
@event_node(Namespace.LOCATION)
|
||||
def _location_received(self, _con, _stanza, properties):
|
||||
if properties.pubsub_event.retracted:
|
||||
return
|
||||
|
||||
data = properties.pubsub_event.data
|
||||
for contact in app.contacts.get_contacts(self._account,
|
||||
str(properties.jid)):
|
||||
if data is not None:
|
||||
contact.pep[PEPEventType.LOCATION] = data
|
||||
else:
|
||||
contact.pep.pop(PEPEventType.LOCATION, None)
|
||||
|
||||
if properties.is_self_message:
|
||||
if data is not None:
|
||||
self._con.pep[PEPEventType.LOCATION] = data
|
||||
else:
|
||||
self._con.pep.pop(PEPEventType.LOCATION, None)
|
||||
self._current_location = data
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('location-received',
|
||||
account=self._account,
|
||||
jid=properties.jid.bare,
|
||||
location=data,
|
||||
is_self_message=properties.is_self_message))
|
||||
|
||||
@store_publish
|
||||
def set_location(self, location):
|
||||
self._current_location = location
|
||||
self._log.info('Send %s', location)
|
||||
self._nbxmpp('Location').set_location(location)
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return UserLocation(*args, **kwargs), 'UserLocation'
|
91
gajim/common/modules/user_mood.py
Normal file
91
gajim/common/modules/user_mood.py
Normal file
|
@ -0,0 +1,91 @@
|
|||
# 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-0107: User Mood
|
||||
|
||||
from typing import Any
|
||||
from typing import Tuple
|
||||
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.structs import MoodData
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.modules.util import event_node
|
||||
from gajim.common.const import PEPEventType
|
||||
|
||||
|
||||
class UserMood(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'Mood'
|
||||
_nbxmpp_methods = [
|
||||
'set_mood',
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
self._register_pubsub_handler(self._mood_received)
|
||||
|
||||
self._current_mood = None
|
||||
|
||||
def get_current_mood(self):
|
||||
return self._current_mood
|
||||
|
||||
@event_node(Namespace.MOOD)
|
||||
def _mood_received(self, _con, _stanza, properties):
|
||||
if properties.pubsub_event.retracted:
|
||||
return
|
||||
|
||||
data = properties.pubsub_event.data
|
||||
for contact in app.contacts.get_contacts(self._account,
|
||||
str(properties.jid)):
|
||||
if data is not None:
|
||||
contact.pep[PEPEventType.MOOD] = data
|
||||
else:
|
||||
contact.pep.pop(PEPEventType.MOOD, None)
|
||||
|
||||
if properties.is_self_message:
|
||||
if data is not None:
|
||||
self._con.pep[PEPEventType.MOOD] = data
|
||||
else:
|
||||
self._con.pep.pop(PEPEventType.MOOD, None)
|
||||
self._current_mood = data
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('mood-received',
|
||||
account=self._account,
|
||||
jid=properties.jid.bare,
|
||||
mood=data,
|
||||
is_self_message=properties.is_self_message))
|
||||
|
||||
def set_mood(self, mood):
|
||||
if mood is not None:
|
||||
mood = MoodData(mood, None)
|
||||
|
||||
if mood == self._current_mood:
|
||||
return
|
||||
|
||||
self._current_mood = mood
|
||||
|
||||
if mood is None:
|
||||
self._log.info('Remove user mood')
|
||||
else:
|
||||
self._log.info('Set %s', mood)
|
||||
|
||||
self._nbxmpp('Mood').set_mood(mood)
|
||||
|
||||
|
||||
def get_instance(*args: Any, **kwargs: Any) -> Tuple[UserMood, str]:
|
||||
return UserMood(*args, **kwargs), 'UserMood'
|
65
gajim/common/modules/user_nickname.py
Normal file
65
gajim/common/modules/user_nickname.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
# 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-0172: User Nickname
|
||||
|
||||
from typing import Any
|
||||
from typing import Tuple
|
||||
|
||||
from nbxmpp.namespaces import Namespace
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.modules.util import event_node
|
||||
|
||||
|
||||
class UserNickname(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'Nickname'
|
||||
_nbxmpp_methods = [
|
||||
'set_nickname',
|
||||
'set_access_model',
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
self._register_pubsub_handler(self._nickname_received)
|
||||
|
||||
@event_node(Namespace.NICK)
|
||||
def _nickname_received(self, _con, _stanza, properties):
|
||||
if properties.pubsub_event.retracted:
|
||||
return
|
||||
|
||||
nick = properties.pubsub_event.data
|
||||
if properties.is_self_message:
|
||||
if nick is None:
|
||||
nick = app.settings.get_account_setting(self._account, 'name')
|
||||
app.nicks[self._account] = nick
|
||||
|
||||
for contact in app.contacts.get_contacts(self._account,
|
||||
str(properties.jid)):
|
||||
contact.contact_name = nick
|
||||
|
||||
self._log.info('Nickname for %s: %s', properties.jid, nick)
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('nickname-received',
|
||||
account=self._account,
|
||||
jid=properties.jid.bare,
|
||||
nickname=nick))
|
||||
|
||||
|
||||
def get_instance(*args: Any, **kwargs: Any) -> Tuple[UserNickname, str]:
|
||||
return UserNickname(*args, **kwargs), 'UserNickname'
|
124
gajim/common/modules/user_tune.py
Normal file
124
gajim/common/modules/user_tune.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
# 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-0118: User Tune
|
||||
|
||||
from typing import Any
|
||||
from typing import Tuple
|
||||
|
||||
from nbxmpp.namespaces import Namespace
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common import ged
|
||||
from gajim.common.nec import NetworkEvent
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.modules.util import event_node
|
||||
from gajim.common.modules.util import store_publish
|
||||
from gajim.common.const import PEPEventType
|
||||
from gajim.common.dbus.music_track import MusicTrackListener
|
||||
from gajim.common.helpers import event_filter
|
||||
|
||||
|
||||
class UserTune(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'Tune'
|
||||
_nbxmpp_methods = [
|
||||
'set_tune',
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
self._register_pubsub_handler(self._tune_received)
|
||||
self._tune_data = None
|
||||
|
||||
self.register_events([
|
||||
('music-track-changed', ged.CORE, self._on_music_track_changed),
|
||||
('signed-in', ged.CORE, self._on_signed_in),
|
||||
])
|
||||
|
||||
def get_current_tune(self):
|
||||
return self._tune_data
|
||||
|
||||
@event_node(Namespace.TUNE)
|
||||
def _tune_received(self, _con, _stanza, properties):
|
||||
if properties.pubsub_event.retracted:
|
||||
return
|
||||
|
||||
data = properties.pubsub_event.data
|
||||
for contact in app.contacts.get_contacts(self._account,
|
||||
str(properties.jid)):
|
||||
if data is not None:
|
||||
contact.pep[PEPEventType.TUNE] = data
|
||||
else:
|
||||
contact.pep.pop(PEPEventType.TUNE, None)
|
||||
|
||||
if properties.is_self_message:
|
||||
if data is not None:
|
||||
self._con.pep[PEPEventType.TUNE] = data
|
||||
else:
|
||||
self._con.pep.pop(PEPEventType.TUNE, None)
|
||||
self._tune_data = data
|
||||
|
||||
app.nec.push_incoming_event(
|
||||
NetworkEvent('tune-received',
|
||||
account=self._account,
|
||||
jid=properties.jid.bare,
|
||||
tune=data,
|
||||
is_self_message=properties.is_self_message))
|
||||
|
||||
@store_publish
|
||||
def set_tune(self, tune):
|
||||
if not self._con.get_module('PEP').supported:
|
||||
return
|
||||
|
||||
if not app.settings.get_account_setting(self._account, 'publish_tune'):
|
||||
return
|
||||
|
||||
if tune == self._tune_data:
|
||||
return
|
||||
|
||||
self._tune_data = tune
|
||||
|
||||
self._log.info('Send %s', tune)
|
||||
self._nbxmpp('Tune').set_tune(tune)
|
||||
|
||||
def set_enabled(self, enable):
|
||||
if enable:
|
||||
app.settings.set_account_setting(self._account,
|
||||
'publish_tune',
|
||||
True)
|
||||
self._publish_current_tune()
|
||||
|
||||
else:
|
||||
self.set_tune(None)
|
||||
app.settings.set_account_setting(self._account,
|
||||
'publish_tune',
|
||||
False)
|
||||
|
||||
def _publish_current_tune(self):
|
||||
self.set_tune(MusicTrackListener.get().current_tune)
|
||||
|
||||
@event_filter(['account'])
|
||||
def _on_signed_in(self, _event):
|
||||
self._publish_current_tune()
|
||||
|
||||
def _on_music_track_changed(self, event):
|
||||
if self._tune_data == event.info:
|
||||
return
|
||||
|
||||
self.set_tune(event.info)
|
||||
|
||||
|
||||
def get_instance(*args: Any, **kwargs: Any) -> Tuple[UserTune, str]:
|
||||
return UserTune(*args, **kwargs), 'UserTune'
|
106
gajim/common/modules/util.py
Normal file
106
gajim/common/modules/util.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
# 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/>.
|
||||
|
||||
# Util module
|
||||
|
||||
from typing import Union
|
||||
|
||||
from logging import LoggerAdapter
|
||||
from functools import wraps
|
||||
from functools import partial
|
||||
|
||||
from nbxmpp.task import Task
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.const import EME_MESSAGES
|
||||
|
||||
|
||||
def from_xs_boolean(value: Union[str, bool]) -> bool:
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
|
||||
if value in ('1', 'true', 'True'):
|
||||
return True
|
||||
|
||||
if value in ('0', 'false', 'False', ''):
|
||||
return False
|
||||
|
||||
raise ValueError('Cant convert %s to python boolean' % value)
|
||||
|
||||
|
||||
def to_xs_boolean(value: Union[bool, None]) -> str:
|
||||
# Convert to xs:boolean ('true', 'false')
|
||||
# from a python boolean (True, False) or None
|
||||
if value is True:
|
||||
return 'true'
|
||||
|
||||
if value is False:
|
||||
return 'false'
|
||||
|
||||
if value is None:
|
||||
return 'false'
|
||||
|
||||
raise ValueError(
|
||||
'Cant convert %s to xs:boolean' % value)
|
||||
|
||||
|
||||
def event_node(node):
|
||||
def event_node_decorator(func):
|
||||
@wraps(func)
|
||||
def func_wrapper(self, _con, _stanza, properties):
|
||||
if not properties.is_pubsub_event:
|
||||
return
|
||||
if properties.pubsub_event.node != node:
|
||||
return
|
||||
func(self, _con, _stanza, properties)
|
||||
|
||||
return func_wrapper
|
||||
return event_node_decorator
|
||||
|
||||
|
||||
def store_publish(func):
|
||||
@wraps(func)
|
||||
def func_wrapper(self, *args, **kwargs):
|
||||
# pylint: disable=protected-access
|
||||
if not app.account_is_connected(self._account):
|
||||
self._stored_publish = partial(func, self, *args, **kwargs)
|
||||
return None
|
||||
return func(self, *args, **kwargs)
|
||||
return func_wrapper
|
||||
|
||||
|
||||
def get_eme_message(eme_data):
|
||||
try:
|
||||
return EME_MESSAGES[eme_data.namespace]
|
||||
except KeyError:
|
||||
return EME_MESSAGES['fallback'] % eme_data.name
|
||||
|
||||
|
||||
class LogAdapter(LoggerAdapter):
|
||||
def process(self, msg, kwargs):
|
||||
return '(%s) %s' % (self.extra['account'], msg), kwargs
|
||||
|
||||
|
||||
def as_task(func):
|
||||
@wraps(func)
|
||||
def func_wrapper(self, *args, callback=None, user_data=None, **kwargs):
|
||||
task_ = Task(func(self, *args, **kwargs))
|
||||
app.register_task(self, task_)
|
||||
task_.set_finalize_func(app.remove_task, id(self))
|
||||
task_.set_user_data(user_data)
|
||||
if callback is not None:
|
||||
task_.add_done_callback(callback)
|
||||
task_.start()
|
||||
return task_
|
||||
return func_wrapper
|
33
gajim/common/modules/vcard4.py
Normal file
33
gajim/common/modules/vcard4.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
# 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-0292: vCard4 Over XMPP
|
||||
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class VCard4(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'VCard4'
|
||||
_nbxmpp_methods = [
|
||||
'request_vcard',
|
||||
'set_vcard',
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return VCard4(*args, **kwargs), 'VCard4'
|
204
gajim/common/modules/vcard_avatars.py
Normal file
204
gajim/common/modules/vcard_avatars.py
Normal file
|
@ -0,0 +1,204 @@
|
|||
# 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-0153: vCard-Based Avatars
|
||||
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
from nbxmpp.const import AvatarState
|
||||
from nbxmpp.modules.util import is_error
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.modules.util import as_task
|
||||
|
||||
|
||||
class VCardAvatars(BaseModule):
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
self._requested_shas = []
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='presence',
|
||||
callback=self._presence_received,
|
||||
ns=Namespace.VCARD_UPDATE,
|
||||
priority=51),
|
||||
]
|
||||
|
||||
self.avatar_conversion_available = False
|
||||
|
||||
def pass_disco(self, info):
|
||||
is_available = Namespace.VCARD_CONVERSION in info.features
|
||||
self.avatar_conversion_available = is_available
|
||||
self._log.info('Discovered Avatar Conversion')
|
||||
|
||||
@as_task
|
||||
def _request_vcard(self, jid, expected_sha, type_):
|
||||
_task = yield
|
||||
|
||||
vcard = yield self._con.get_module('VCardTemp').request_vcard(jid=jid)
|
||||
|
||||
if is_error(vcard):
|
||||
self._log.warning(vcard)
|
||||
return
|
||||
|
||||
avatar, avatar_sha = vcard.get_avatar()
|
||||
if avatar is None:
|
||||
self._log.warning('Avatar missing: %s %s', jid, expected_sha)
|
||||
return
|
||||
|
||||
if expected_sha != avatar_sha:
|
||||
self._log.warning('Avatar mismatch: %s %s != %s',
|
||||
jid,
|
||||
expected_sha,
|
||||
avatar_sha)
|
||||
return
|
||||
|
||||
self._log.info('Received: %s %s', jid, avatar_sha)
|
||||
app.interface.save_avatar(avatar)
|
||||
|
||||
if type_ == 'contact':
|
||||
self._con.get_module('Roster').set_avatar_sha(jid, avatar_sha)
|
||||
app.contacts.set_avatar(self._account, jid, avatar_sha)
|
||||
app.interface.update_avatar(self._account, jid)
|
||||
|
||||
elif type_ == 'muc':
|
||||
app.storage.cache.set_muc_avatar_sha(jid, avatar_sha)
|
||||
app.contacts.set_avatar(self._account, jid, avatar_sha)
|
||||
app.interface.update_avatar(self._account, jid, room_avatar=True)
|
||||
|
||||
elif type_ == 'muc-user':
|
||||
contact = app.contacts.get_gc_contact(self._account,
|
||||
jid.bare,
|
||||
jid.resource)
|
||||
if contact is not None:
|
||||
contact.avatar_sha = avatar_sha
|
||||
app.interface.update_avatar(contact=contact)
|
||||
|
||||
def _presence_received(self, _con, _stanza, properties):
|
||||
if not properties.type.is_available:
|
||||
return
|
||||
|
||||
if properties.avatar_state in (AvatarState.IGNORE,
|
||||
AvatarState.NOT_READY):
|
||||
return
|
||||
|
||||
if self._con.get_own_jid().bare_match(properties.jid):
|
||||
return
|
||||
|
||||
if properties.from_muc:
|
||||
self._gc_update_received(properties)
|
||||
else:
|
||||
# Check if presence is from a MUC service
|
||||
contact = app.contacts.get_groupchat_contact(self._account,
|
||||
str(properties.jid))
|
||||
self._update_received(properties, room=contact is not None)
|
||||
|
||||
def muc_disco_info_update(self, disco_info):
|
||||
if not disco_info.supports(Namespace.VCARD):
|
||||
return
|
||||
|
||||
field_var = '{http://modules.prosody.im/mod_vcard_muc}avatar#sha1'
|
||||
if not disco_info.has_field(Namespace.MUC_INFO, field_var):
|
||||
# Workaround so we don’t delete the avatar for servers that don’t
|
||||
# support sha in disco info. Once there is a accepted XEP this
|
||||
# can be removed
|
||||
return
|
||||
|
||||
avatar_sha = disco_info.get_field_value(Namespace.MUC_INFO, field_var)
|
||||
state = AvatarState.EMPTY if not avatar_sha else AvatarState.ADVERTISED
|
||||
self._process_update(str(disco_info.jid), state, avatar_sha, True)
|
||||
|
||||
def _update_received(self, properties, room=False):
|
||||
self._process_update(properties.jid.bare,
|
||||
properties.avatar_state,
|
||||
properties.avatar_sha,
|
||||
room)
|
||||
|
||||
def _process_update(self, jid, state, avatar_sha, room):
|
||||
if state == AvatarState.EMPTY:
|
||||
# Empty <photo/> tag, means no avatar is advertised
|
||||
self._log.info('%s has no avatar published', jid)
|
||||
app.contacts.set_avatar(self._account, jid, None)
|
||||
|
||||
if room:
|
||||
app.storage.cache.set_muc_avatar_sha(jid, None)
|
||||
else:
|
||||
self._con.get_module('Roster').set_avatar_sha(jid, None)
|
||||
app.interface.update_avatar(self._account, jid, room_avatar=room)
|
||||
else:
|
||||
self._log.info('Update: %s %s', jid, avatar_sha)
|
||||
current_sha = app.contacts.get_avatar_sha(self._account, jid)
|
||||
|
||||
if avatar_sha == current_sha:
|
||||
self._log.info('Avatar already known: %s %s', jid, avatar_sha)
|
||||
return
|
||||
|
||||
if app.interface.avatar_exists(avatar_sha):
|
||||
# Check if the avatar is already in storage
|
||||
self._log.info('Found avatar in storage')
|
||||
if room:
|
||||
app.storage.cache.set_muc_avatar_sha(jid, avatar_sha)
|
||||
else:
|
||||
self._con.get_module('Roster').set_avatar_sha(jid,
|
||||
avatar_sha)
|
||||
app.contacts.set_avatar(self._account, jid, avatar_sha)
|
||||
app.interface.update_avatar(
|
||||
self._account, jid, room_avatar=room)
|
||||
return
|
||||
|
||||
if avatar_sha not in self._requested_shas:
|
||||
self._requested_shas.append(avatar_sha)
|
||||
if room:
|
||||
self._request_vcard(jid, avatar_sha, 'muc')
|
||||
else:
|
||||
self._request_vcard(jid, avatar_sha, 'contact')
|
||||
|
||||
def _gc_update_received(self, properties):
|
||||
nick = properties.jid.resource
|
||||
|
||||
gc_contact = app.contacts.get_gc_contact(
|
||||
self._account, properties.jid.bare, nick)
|
||||
|
||||
if gc_contact is None:
|
||||
self._log.error('no gc contact found: %s', nick)
|
||||
return
|
||||
|
||||
if properties.avatar_state == AvatarState.EMPTY:
|
||||
# Empty <photo/> tag, means no avatar is advertised
|
||||
self._log.info('%s has no avatar published', nick)
|
||||
gc_contact.avatar_sha = None
|
||||
app.interface.update_avatar(contact=gc_contact)
|
||||
else:
|
||||
self._log.info('Update: %s %s', nick, properties.avatar_sha)
|
||||
if not app.interface.avatar_exists(properties.avatar_sha):
|
||||
if properties.avatar_sha not in self._requested_shas:
|
||||
app.log('avatar').info('Request: %s', nick)
|
||||
self._requested_shas.append(properties.avatar_sha)
|
||||
self._request_vcard(properties.jid,
|
||||
properties.avatar_sha,
|
||||
'muc-user')
|
||||
return
|
||||
|
||||
if gc_contact.avatar_sha != properties.avatar_sha:
|
||||
self._log.info('%s changed their Avatar: %s',
|
||||
nick, properties.avatar_sha)
|
||||
gc_contact.avatar_sha = properties.avatar_sha
|
||||
app.interface.update_avatar(contact=gc_contact)
|
||||
else:
|
||||
self._log.info('Avatar already known: %s', nick)
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return VCardAvatars(*args, **kwargs), 'VCardAvatars'
|
45
gajim/common/modules/vcard_temp.py
Normal file
45
gajim/common/modules/vcard_temp.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
# 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-0054: vcard-temp
|
||||
|
||||
from nbxmpp.namespaces import Namespace
|
||||
|
||||
from gajim.common.modules.base import BaseModule
|
||||
|
||||
|
||||
class VCardTemp(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'VCardTemp'
|
||||
_nbxmpp_methods = [
|
||||
'request_vcard',
|
||||
'set_vcard',
|
||||
]
|
||||
|
||||
def __init__(self, con):
|
||||
BaseModule.__init__(self, con)
|
||||
|
||||
self._own_vcard = None
|
||||
self.supported = False
|
||||
|
||||
def pass_disco(self, info):
|
||||
if Namespace.VCARD not in info.features:
|
||||
return
|
||||
|
||||
self.supported = True
|
||||
self._log.info('Discovered vcard-temp: %s', info.jid)
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return VCardTemp(*args, **kwargs), 'VCardTemp'
|
Loading…
Add table
Add a link
Reference in a new issue