9578053 Jan 22 2022 distfiles.gentoo.org/distfiles/gajim-1.3.3-2.tar.gz

This commit is contained in:
emdee 2022-10-19 18:09:31 +00:00
parent a5b3822651
commit 4c1b226bff
1045 changed files with 753037 additions and 18 deletions

View 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

View 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'

View 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'

View 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'

View 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, cant 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, cant 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()

View 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'

View 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'

View 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'

View 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'

View 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'

View 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'

View 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'

View 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:
# Dont 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:
# Dont 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:
# Dont 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'

View 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'

View 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'

View 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'

View 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'

View 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'

View 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
View 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'

View 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'

View 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'

View 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
View 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'

View 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
# Dont 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'

View 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'

View 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 dont 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
View 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 dont 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, dont 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'

View 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'

View 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'

View 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'

View 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'

View 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'

View 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'

View 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'

View 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'

View 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'

View 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'

View 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'

View 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'

View 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'

View 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'

View 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'

View 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'

View 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'

View 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

View 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'

View 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 dont delete the avatar for servers that dont
# 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'

View 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'