gajim-plural/gajim/common/connection_handlers.py
Philipp Hörist 3e73ee93e1 Add XEP-0398 optimizations
- If the server implements XEP-0398 we dont need to add the avatar sha
anymore, the server adds it for us.
- It also means we dont have to query our own avatar from vcard at start
because the server tells us the avatar sha that is published with the inital
presence reflection
2018-10-07 00:46:34 +02:00

654 lines
25 KiB
Python

# -*- coding:utf-8 -*-
## src/common/connection_handlers.py
##
## 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 operator
from time import time as time_time
import nbxmpp
from gajim.common import modules
from gajim.common import helpers
from gajim.common import app
from gajim.common import jingle_xtls
from gajim.common.caps_cache import muc_caps_cache
from gajim.common.protocol.bytestream import ConnectionSocks5Bytestream
from gajim.common.protocol.bytestream import ConnectionIBBytestream
from gajim.common.connection_handlers_events import *
from gajim.common import ged
from gajim.common.nec import NetworkEvent
from gajim.common.const import KindConstant
from gajim.common.jingle import ConnectionJingle
import logging
log = logging.getLogger('gajim.c.connection_handlers')
# kind of events we can wait for an answer
AGENT_REMOVED = 'agent_removed'
# basic connection handlers used here and in zeroconf
class ConnectionHandlersBase:
def __init__(self):
# List of IDs we are waiting answers for {id: (type_of_request, data), }
self.awaiting_answers = {}
# List of IDs that will produce a timeout is answer doesn't arrive
# {time_of_the_timeout: (id, message to send to gui), }
self.awaiting_timeouts = {}
# keep track of sessions this connection has with other JIDs
self.sessions = {}
# IDs of sent messages (https://trac.gajim.org/ticket/8222)
self.sent_message_ids = []
# We decrypt GPG messages one after the other. Keep queue in mem
self.gpg_messages_to_decrypt = []
app.ged.register_event_handler('presence-received', ged.CORE,
self._nec_presence_received)
app.ged.register_event_handler('gc-message-received', ged.CORE,
self._nec_gc_message_received)
def cleanup(self):
app.ged.remove_event_handler('presence-received', ged.CORE,
self._nec_presence_received)
app.ged.remove_event_handler('gc-message-received', ged.CORE,
self._nec_gc_message_received)
def _nec_presence_received(self, obj):
account = obj.conn.name
if account != self.name:
return
jid = obj.jid
resource = obj.resource or ''
statuss = ['offline', 'error', 'online', 'chat', 'away', 'xa', 'dnd',
'invisible']
obj.old_show = 0
obj.new_show = statuss.index(obj.show)
obj.contact_list = []
highest = app.contacts.get_contact_with_highest_priority(account, jid)
obj.was_highest = (highest and highest.resource == resource)
# Update contact
obj.contact_list = app.contacts.get_contacts(account, jid)
obj.contact = None
resources = []
for c in obj.contact_list:
resources.append(c.resource)
if c.resource == resource:
obj.contact = c
break
if obj.contact:
if obj.contact.show in statuss:
obj.old_show = statuss.index(obj.contact.show)
# nick changed
if obj.contact_nickname is not None and \
obj.contact.contact_name != obj.contact_nickname:
obj.contact.contact_name = obj.contact_nickname
obj.need_redraw = True
elif obj.old_show != obj.new_show or obj.contact.status != \
obj.status:
obj.need_redraw = True
else:
obj.contact = app.contacts.get_first_contact_from_jid(account,
jid)
if not obj.contact:
# Presence of another resource of our jid
# Create self contact and add to roster
if resource == obj.conn.server_resource:
return
# Ignore offline presence of unknown self resource
if obj.new_show < 2:
return
obj.contact = app.contacts.create_self_contact(jid=jid,
account=account, show=obj.show, status=obj.status,
priority=obj.prio, keyID=obj.keyID,
resource=obj.resource)
app.contacts.add_contact(account, obj.contact)
obj.contact_list.append(obj.contact)
elif obj.contact.show in statuss:
obj.old_show = statuss.index(obj.contact.show)
if (resources != [''] and (len(obj.contact_list) != 1 or \
obj.contact_list[0].show not in ('not in roster', 'offline'))) and \
not app.jid_is_transport(jid):
# Another resource of an existing contact connected
obj.old_show = 0
obj.contact = app.contacts.copy_contact(obj.contact)
obj.contact_list.append(obj.contact)
obj.contact.resource = resource
obj.need_add_in_roster = True
if not app.jid_is_transport(jid) and len(obj.contact_list) == 1:
# It's not an agent
if obj.old_show == 0 and obj.new_show > 1:
if not jid in app.newly_added[account]:
app.newly_added[account].append(jid)
if jid in app.to_be_removed[account]:
app.to_be_removed[account].remove(jid)
elif obj.old_show > 1 and obj.new_show == 0 and \
obj.conn.connected > 1:
if not jid in app.to_be_removed[account]:
app.to_be_removed[account].append(jid)
if jid in app.newly_added[account]:
app.newly_added[account].remove(jid)
obj.need_redraw = True
obj.contact.show = obj.show
obj.contact.status = obj.status
obj.contact.priority = obj.prio
attached_keys = app.config.get_per('accounts', account,
'attached_gpg_keys').split()
if jid in attached_keys:
obj.contact.keyID = attached_keys[attached_keys.index(jid) + 1]
else:
# Do not override assigned key
obj.contact.keyID = obj.keyID
obj.contact.contact_nickname = obj.contact_nickname
obj.contact.idle_time = obj.idle_time
if app.jid_is_transport(jid):
return
# It isn't an agent
# reset chatstate if needed:
# (when contact signs out or has errors)
if obj.show in ('offline', 'error'):
obj.contact.our_chatstate = obj.contact.chatstate = None
# TODO: This causes problems when another
# resource signs off!
self.stop_all_active_file_transfers(obj.contact)
if app.config.get('log_contact_status_changes') and \
app.config.should_log(self.name, obj.jid):
show = app.logger.convert_show_values_to_db_api_values(obj.show)
if show is not None:
app.logger.insert_into_logs(self.name,
nbxmpp.JID(obj.jid).getStripped(),
time_time(),
KindConstant.STATUS,
message=obj.status,
show=show)
def _check_for_mam_compliance(self, room_jid, stanza_id):
namespace = muc_caps_cache.get_mam_namespace(room_jid)
if stanza_id is None and namespace == nbxmpp.NS_MAM_2:
log.warning('%s announces mam:2 without stanza-id', room_jid)
def _nec_gc_message_received(self, obj):
if obj.conn.name != self.name:
return
self._check_for_mam_compliance(obj.jid, obj.stanza_id)
if (app.config.should_log(obj.conn.name, obj.jid) and
obj.msgtxt and obj.nick):
# if not obj.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.logger.insert_into_logs(self.name,
obj.jid,
obj.timestamp,
KindConstant.GC_MSG,
message=obj.msgtxt,
contact_name=obj.nick,
additional_data=obj.additional_data,
stanza_id=obj.stanza_id)
app.logger.set_room_last_message_time(obj.room_jid, obj.timestamp)
self.get_module('MAM').save_archive_id(
obj.room_jid, obj.stanza_id, obj.timestamp)
# process and dispatch an error message
def dispatch_error_message(self, msg, msgtxt, session, frm, tim):
error_msg = msg.getErrorMsg()
if not error_msg:
error_msg = msgtxt
msgtxt = None
subject = msg.getSubject()
if session.is_loggable():
app.logger.insert_into_logs(self.name,
nbxmpp.JID(frm).getStripped(),
tim,
KindConstant.ERROR,
message=error_msg,
subject=subject)
app.nec.push_incoming_event(MessageErrorEvent(None, conn=self,
fjid=frm, error_code=msg.getErrorCode(), error_msg=error_msg,
msg=msgtxt, time_=tim, session=session, stanza=msg))
def get_sessions(self, jid):
"""
Get all sessions for the given full jid
"""
if not app.interface.is_pm_contact(jid, self.name):
jid = app.get_jid_without_resource(jid)
try:
return list(self.sessions[jid].values())
except KeyError:
return []
def get_or_create_session(self, fjid, thread_id):
"""
Return an existing session between this connection and 'jid', returns a
new one if none exist
"""
pm = True
jid = fjid
if not app.interface.is_pm_contact(fjid, self.name):
pm = False
jid = app.get_jid_without_resource(fjid)
session = self.find_session(jid, thread_id)
if session:
return session
if pm:
return self.make_new_session(fjid, thread_id, type_='pm')
else:
return self.make_new_session(fjid, thread_id)
def find_session(self, jid, thread_id):
try:
if not thread_id:
return self.find_null_session(jid)
else:
return self.sessions[jid][thread_id]
except KeyError:
return None
def terminate_sessions(self):
self.sessions = {}
def delete_session(self, jid, thread_id):
if not jid in self.sessions:
jid = app.get_jid_without_resource(jid)
if not jid in self.sessions:
return
del self.sessions[jid][thread_id]
if not self.sessions[jid]:
del self.sessions[jid]
def find_null_session(self, jid):
"""
Find all of the sessions between us and a remote jid in which we haven't
received a thread_id yet and returns the session that we last sent a
message to
"""
sessions = list(self.sessions[jid].values())
# sessions that we haven't received a thread ID in
idless = [s for s in sessions if not s.received_thread_id]
# filter out everything except the default session type
chat_sessions = [s for s in idless if isinstance(s,
app.default_session_type)]
if chat_sessions:
# return the session that we last sent a message in
return sorted(chat_sessions, key=operator.attrgetter('last_send'))[
-1]
else:
return None
def get_latest_session(self, jid):
"""
Get the session that we last sent a message to
"""
if jid not in self.sessions:
return None
sessions = self.sessions[jid].values()
if not sessions:
return None
return sorted(sessions, key=operator.attrgetter('last_send'))[-1]
def find_controlless_session(self, jid, resource=None):
"""
Find an active session that doesn't have a control attached
"""
try:
sessions = list(self.sessions[jid].values())
# filter out everything except the default session type
chat_sessions = [s for s in sessions if isinstance(s,
app.default_session_type)]
orphaned = [s for s in chat_sessions if not s.control]
if resource:
orphaned = [s for s in orphaned if s.resource == resource]
return orphaned[0]
except (KeyError, IndexError):
return None
def make_new_session(self, jid, thread_id=None, type_='chat', cls=None):
"""
Create and register a new session
thread_id=None to generate one.
type_ should be 'chat' or 'pm'.
"""
if not cls:
cls = app.default_session_type
sess = cls(self, nbxmpp.JID(jid), thread_id, type_)
# determine if this session is a pm session
# if not, discard the resource so that all sessions are stored bare
if not type_ == 'pm':
jid = app.get_jid_without_resource(jid)
if not jid in self.sessions:
self.sessions[jid] = {}
self.sessions[jid][sess.thread_id] = sess
return sess
class ConnectionHandlers(ConnectionSocks5Bytestream,
ConnectionHandlersBase,
ConnectionJingle, ConnectionIBBytestream):
def __init__(self):
ConnectionSocks5Bytestream.__init__(self)
ConnectionIBBytestream.__init__(self)
# Handle presences BEFORE caps
app.nec.register_incoming_event(PresenceReceivedEvent)
ConnectionJingle.__init__(self)
ConnectionHandlersBase.__init__(self)
# IDs of disco#items requests
self.disco_items_ids = []
# IDs of disco#info requests
self.disco_info_ids = []
self.continue_connect_info = None
# If handlers have been registered
self.handlers_registered = False
app.nec.register_incoming_event(StreamConflictReceivedEvent)
app.nec.register_incoming_event(NotificationEvent)
app.ged.register_event_handler('agent-removed', ged.CORE,
self._nec_agent_removed)
def cleanup(self):
ConnectionHandlersBase.cleanup(self)
app.ged.remove_event_handler('agent-removed', ged.CORE,
self._nec_agent_removed)
def _ErrorCB(self, con, iq_obj):
log.debug('ErrorCB')
app.nec.push_incoming_event(IqErrorReceivedEvent(None, conn=self,
stanza=iq_obj))
def _IqCB(self, con, iq_obj):
id_ = iq_obj.getID()
app.nec.push_incoming_event(NetworkEvent('raw-iq-received',
conn=self, stanza=iq_obj))
# Check if we were waiting a timeout for this id
found_tim = None
for tim in self.awaiting_timeouts:
if id_ == self.awaiting_timeouts[tim][0]:
found_tim = tim
break
if found_tim:
del self.awaiting_timeouts[found_tim]
if id_ not in self.awaiting_answers:
return
if self.awaiting_answers[id_][0] == AGENT_REMOVED:
jid = self.awaiting_answers[id_][1]
app.nec.push_incoming_event(AgentRemovedEvent(None, conn=self,
agent=jid))
del self.awaiting_answers[id_]
def _dispatch_gc_msg_with_captcha(self, stanza, msg_obj):
msg_obj.stanza = stanza
app.nec.push_incoming_event(GcMessageReceivedEvent(None,
conn=self, msg_obj=msg_obj))
def _on_bob_received(self, conn, 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=nbxmpp.NS_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 occured, 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')
data = iq.addChild(name='data', attrs={'cid': cid},
namespace=nbxmpp.NS_BOB)
self.connection.SendAndCallForResponse(iq, self._on_bob_received,
{'cid': cid})
def _nec_agent_removed(self, obj):
if obj.conn.name != self.name:
return
for jid in obj.jid_list:
log.debug('Removing contact %s due to unregistered transport %s' % \
(jid, obj.agent))
self.get_module('Presence').unsubscribe(jid)
# Transport contacts can't have 2 resources
if jid in app.to_be_removed[self.name]:
# This way we'll really remove it
app.to_be_removed[self.name].remove(jid)
def discover_ft_proxies(self):
cfg_proxies = app.config.get_per('accounts', self.name,
'file_transfer_proxies')
our_jid = helpers.parse_jid(app.get_jid_from_account(self.name) + \
'/' + self.server_resource)
testit = app.config.get_per('accounts', self.name,
'test_ft_proxies_on_startup')
if cfg_proxies:
proxies = [e.strip() for e in cfg_proxies.split(',')]
for proxy in proxies:
app.proxy65_manager.resolve(proxy, self.connection, our_jid,
testit=testit)
def send_first_presence(self):
if self.connected > 1 and self.continue_connect_info:
msg = self.continue_connect_info[1]
sign_msg = self.continue_connect_info[2]
signed = ''
send_first_presence = True
if sign_msg:
signed = self.get_signed_presence(msg,
self._send_first_presence)
if signed is None:
app.nec.push_incoming_event(GPGPasswordRequiredEvent(None,
conn=self, callback=self._send_first_presence))
# _send_first_presence will be called when user enter
# passphrase
send_first_presence = False
if send_first_presence:
self._send_first_presence(signed)
def _send_first_presence(self, signed=''):
show = self.continue_connect_info[0]
msg = self.continue_connect_info[1]
sign_msg = self.continue_connect_info[2]
if sign_msg and not signed:
signed = self.get_signed_presence(msg)
if signed is None:
app.nec.push_incoming_event(BadGPGPassphraseEvent(None,
conn=self))
self.USE_GPG = False
signed = ''
self.connected = app.SHOW_LIST.index(show)
sshow = helpers.get_xmpp_show(show)
# send our presence
if show == 'invisible':
self.send_invisible_presence(msg, signed, True)
return
if show not in ['offline', 'online', 'chat', 'away', 'xa', 'dnd']:
return
priority = app.get_priority(self.name, sshow)
self.get_module('Presence').send_presence(
priority=priority,
show=sshow,
status=msg,
sign=signed)
if self.connection:
self.priority = priority
app.nec.push_incoming_event(OurShowEvent(None, conn=self,
show=show))
if not self.avatar_conversion:
# ask our VCard
self.get_module('VCardTemp').request_vcard()
# Get bookmarks
self.get_module('Bookmarks').get_bookmarks()
# Get annotations from private namespace
self.get_module('Annotations').get_annotations()
# Inform GUI we just signed in
app.nec.push_incoming_event(SignedInEvent(None, conn=self))
self.get_module('PEP').send_stored_publish()
self.continue_connect_info = None
def _PubkeyGetCB(self, con, iq_obj):
log.info('PubkeyGetCB')
jid_from = helpers.get_full_jid_from_iq(iq_obj)
sid = iq_obj.getAttr('id')
jingle_xtls.send_cert(con, jid_from, sid)
raise nbxmpp.NodeProcessed
def _PubkeyResultCB(self, con, iq_obj):
log.info('PubkeyResultCB')
jid_from = helpers.get_full_jid_from_iq(iq_obj)
jingle_xtls.handle_new_cert(con, iq_obj, jid_from)
def _StreamCB(self, con, obj):
log.debug('StreamCB')
app.nec.push_incoming_event(StreamReceivedEvent(None,
conn=self, stanza=obj))
def _register_handlers(self, con, con_type):
# try to find another way to register handlers in each class
# that defines handlers
con.RegisterHandler('iq', self._siSetCB, 'set', nbxmpp.NS_SI)
con.RegisterHandler('iq', self._siErrorCB, 'error', nbxmpp.NS_SI)
con.RegisterHandler('iq', self._siResultCB, 'result', nbxmpp.NS_SI)
con.RegisterHandler('iq', self._bytestreamSetCB, 'set',
nbxmpp.NS_BYTESTREAM)
con.RegisterHandler('iq', self._bytestreamResultCB, 'result',
nbxmpp.NS_BYTESTREAM)
con.RegisterHandler('iq', self._bytestreamErrorCB, 'error',
nbxmpp.NS_BYTESTREAM)
con.RegisterHandlerOnce('iq', self.IBBAllIqHandler)
con.RegisterHandler('iq', self.IBBIqHandler, ns=nbxmpp.NS_IBB)
con.RegisterHandler('message', self.IBBMessageHandler, ns=nbxmpp.NS_IBB)
con.RegisterHandler('iq', self._JingleCB, 'result')
con.RegisterHandler('iq', self._JingleCB, 'error')
con.RegisterHandler('iq', self._JingleCB, 'set', nbxmpp.NS_JINGLE)
con.RegisterHandler('iq', self._ErrorCB, 'error')
con.RegisterHandler('iq', self._IqCB)
con.RegisterHandler('iq', self._ResultCB, 'result')
con.RegisterHandler('unknown', self._StreamCB,
nbxmpp.NS_XMPP_STREAMS, xmlns=nbxmpp.NS_STREAMS)
con.RegisterHandler('iq', self._PubkeyGetCB, 'get',
nbxmpp.NS_PUBKEY_PUBKEY)
con.RegisterHandler('iq', self._PubkeyResultCB, 'result',
nbxmpp.NS_PUBKEY_PUBKEY)
for handler in modules.get_handlers(self):
con.RegisterHandler(*handler)
self.handlers_registered = True
def _unregister_handlers(self):
if not self.connection:
return
for handler in modules.get_handlers(self):
self.connection.UnregisterHandler(*handler)
self.handlers_registered = False