Make detecting the features supported by the remote entity less awful

no more stalling while waiting for a disco info while sending a message.
This commit is contained in:
mathieui 2014-05-15 23:33:51 +02:00
parent 916416a019
commit 97ef9d7fb9
7 changed files with 99 additions and 180 deletions

View file

@ -16,17 +16,12 @@ log = logging.getLogger(__name__)
import getpass import getpass
import sleekxmpp import sleekxmpp
from sleekxmpp.plugins.xep_0184 import XEP_0184 from sleekxmpp.plugins.xep_0184 import XEP_0184
from sleekxmpp.plugins.xep_0030 import StaticDisco
from sleekxmpp.plugins.xep_0115 import StaticCaps
import common import common
import fixes import fixes
from common import safeJID from common import safeJID
from config import config, options from config import config, options
StaticDisco.supports = fixes.xep_30_supports
StaticCaps.supports = fixes.xep_115_supports
class Connection(sleekxmpp.ClientXMPP): class Connection(sleekxmpp.ClientXMPP):
""" """
Receives everything from Jabber and emits the Receives everything from Jabber and emits the

View file

@ -298,7 +298,6 @@ class Core(object):
self.on_theme_config_change) self.on_theme_config_change)
self.add_configuration_handler("", self.on_any_config_change) self.add_configuration_handler("", self.on_any_config_change)
self.reset_iq_errors()
def on_any_config_change(self, option, value): def on_any_config_change(self, option, value):
""" """
@ -741,13 +740,6 @@ class Core(object):
self.timed_events.remove(event) self.timed_events.remove(event)
break break
def reset_iq_errors(self):
"Reset the iq error cache periodically"
fixes.reset_iq_errors()
self.add_timed_event(
timed_events.DelayedEvent(7200, self.reset_iq_errors))
####################### XMPP-related actions ################################## ####################### XMPP-related actions ##################################
def get_status(self): def get_status(self):

View file

@ -10,10 +10,6 @@ from sleekxmpp.xmlstream import ET
import logging import logging
# used to avoid doing numerous useless disco#info requests
# especially with message receipts
IQ_ERRORS = set()
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def has_identity(xmpp, jid, identity): def has_identity(xmpp, jid, identity):
@ -91,97 +87,10 @@ def _filter_add_receipt_request(self, stanza):
if not stanza['body']: if not stanza['body']:
return stanza return stanza
if stanza['to'].resource: # hack
if not self.xmpp['xep_0030'].supports(stanza['to'], if stanza['to'].resource and not hasattr(stanza, '_add_receipt'):
feature='urn:xmpp:receipts',
cached=True):
return stanza return stanza
stanza['request_receipt'] = True stanza['request_receipt'] = True
return stanza return stanza
def xep_30_supports(self, jid, node, ifrom, data):
"""
Check if a JID supports a given feature.
The data parameter may provide:
feature -- The feature to check for support.
local -- If true, then the query is for a JID/node
combination handled by this Sleek instance and
no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the
remove JID to retrieve the info.
cached -- If true, then look for the disco info data from
the local cache system. If no results are found,
send the query as usual. The self.use_cache
setting must be set to true for this option to
be useful. If set to false, then the cache will
be skipped, even if a result has already been
cached. Defaults to false.
"""
feature = data.get('feature', None)
data = {'local': data.get('local', False),
'cached': data.get('cached', True)}
if not feature or jid.full in IQ_ERRORS:
return False
try:
info = self.disco.get_info(jid=jid, node=node,
ifrom=ifrom, **data)
info = self.disco._wrap(ifrom, jid, info, True)
features = info['disco_info']['features']
return feature in features
except:
IQ_ERRORS.add(jid.full)
log.debug('%s added to the list of entities that do'
'not honor disco#info', jid.full)
return False
def xep_115_supports(self, jid, node, ifrom, data):
"""
Check if a JID supports a given feature.
The data parameter may provide:
feature -- The feature to check for support.
local -- If true, then the query is for a JID/node
combination handled by this Sleek instance and
no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the
remove JID to retrieve the info.
cached -- If true, then look for the disco info data from
the local cache system. If no results are found,
send the query as usual. The self.use_cache
setting must be set to true for this option to
be useful. If set to false, then the cache will
be skipped, even if a result has already been
cached. Defaults to false.
"""
feature = data.get('feature', None)
data = {'local': data.get('local', False),
'cached': data.get('cached', True)}
if not feature or jid.full in IQ_ERRORS:
return False
if node in (None, ''):
info = self.caps.get_caps(jid)
if info and feature in info['features']:
return True
try:
info = self.disco.get_info(jid=jid, node=node,
ifrom=ifrom, **data)
info = self.disco._wrap(ifrom, jid, info, True)
return feature in info['disco_info']['features']
except:
IQ_ERRORS.add(jid.full)
log.debug('%s added to the list of entities that do'
'not honor disco#info', jid.full)
return False
def reset_iq_errors():
"reset the iq error cache"
IQ_ERRORS.clear()

View file

@ -1,4 +1,4 @@
from . basetabs import Tab, ChatTab, GapTab from . basetabs import Tab, ChatTab, GapTab, OneToOneTab
from . basetabs import STATE_PRIORITY from . basetabs import STATE_PRIORITY
from . rostertab import RosterInfoTab from . rostertab import RosterInfoTab
from . muctab import MucTab, NS_MUC_USER from . muctab import MucTab, NS_MUC_USER

View file

@ -433,9 +433,6 @@ class ChatTab(Tab):
self.name = jid self.name = jid
self.text_win = None self.text_win = None
self._text_buffer = TextBuffer() self._text_buffer = TextBuffer()
self.remote_wants_chatstates = None # change this to True or False when
# we know that the remote user wants chatstates, or not.
# None means we dont know yet, and we send only "active" chatstates
self.chatstate = None # can be "active", "composing", "paused", "gone", "inactive" self.chatstate = None # can be "active", "composing", "paused", "gone", "inactive"
# We keep a weakref of the event that will set our chatstate to "paused", so that # We keep a weakref of the event that will set our chatstate to "paused", so that
# we can delete it or change it if we need to # we can delete it or change it if we need to
@ -443,7 +440,6 @@ class ChatTab(Tab):
# if thats None, then no paused chatstate was sent recently # if thats None, then no paused chatstate was sent recently
# if thats a weakref returning None, then a paused chatstate was sent # if thats a weakref returning None, then a paused chatstate was sent
# since the last input # since the last input
self.remote_supports_attention = False
# Keeps the last sent message to complete it easily in completion_correct, and to replace it. # Keeps the last sent message to complete it easily in completion_correct, and to replace it.
self.last_sent_message = None self.last_sent_message = None
self.key_func['M-v'] = self.move_separator self.key_func['M-v'] = self.move_separator
@ -503,15 +499,6 @@ class ChatTab(Tab):
identifier=identifier, identifier=identifier,
jid=jid) jid=jid)
def ack_message(self, msg_id):
"""
Ack a message
"""
new_msg = self._text_buffer.ack_message(msg_id)
if new_msg:
self.text_win.modify_message(msg_id, new_msg)
self.core.refresh_window()
def modify_message(self, txt, old_id, new_id, user=None, jid=None, nickname=None): def modify_message(self, txt, old_id, new_id, user=None, jid=None, nickname=None):
self.log_message(txt, nickname, typ=1) self.log_message(txt, nickname, typ=1)
message = self._text_buffer.modify_message(txt, old_id, new_id, time=time, user=user, jid=jid) message = self._text_buffer.modify_message(txt, old_id, new_id, time=time, user=user, jid=jid)
@ -709,3 +696,84 @@ class ChatTab(Tab):
def scroll_separator(self): def scroll_separator(self):
self.text_win.scroll_to_separator() self.text_win.scroll_to_separator()
class OneToOneTab(ChatTab):
def __init__(self, jid=''):
ChatTab.__init__(self, jid)
# change this to True or False when
# we know that the remote user wants chatstates, or not.
# None means we dont know yet, and we send only "active" chatstates
self.remote_wants_chatstates = None
self.remote_supports_attention = False
self.remote_supports_receipts = True
self.check_features()
def ack_message(self, msg_id):
"""
Ack a message
"""
new_msg = self._text_buffer.ack_message(msg_id)
if new_msg:
self.text_win.modify_message(msg_id, new_msg)
self.core.refresh_window()
def check_features(self):
"check the features supported by the other party"
self.core.xmpp.plugin['xep_0030'].get_info(
jid=self.get_dest_jid(), block=False, timeout=5,
callback=self.features_checked)
def command_attention(self, message=''):
"/attention [message]"
if message is not '':
self.command_say(message, attention=True)
else:
msg = self.core.xmpp.make_message(self.get_dest_jid())
msg['type'] = 'chat'
msg['attention'] = True
msg.send()
def command_say(self, line, correct=False, attention=False):
pass
def _feature_attention(self, features):
"Check for the 'attention' features"
if 'urn:xmpp:attention:0' in features:
self.remote_supports_attention = True
self.register_command('attention', self.command_attention,
usage=_('[message]'),
shortdesc=_('Request the attention.'),
desc=_('Attention: Request the attention of '
'the contact. Can also send a message'
' along with the attention.'))
else:
self.remote_supports_attention = False
def _feature_correct(self, features):
"Check for the 'correction' feature"
if not 'urn:xmpp:message-correct:0' in features:
if 'correct' in self.commands:
del self.commands['correct']
elif not 'correct' in self.commands:
self.register_command('correct', self.command_correct,
desc=_('Fix the last message with whatever you want.'),
shortdesc=_('Correct the last message.'),
completion=self.completion_correct)
def _feature_receipts(self, features):
"Check for the 'receipts' feature"
if 'urn:xmpp:receipts' in features:
self.remote_supports_receipts = True
else:
self.remote_supports_receipts = False
def features_checked(self, iq):
"Features check callback"
features = iq['disco_info'].get_features() or []
log.debug('\n\nFEATURES:\n%s\n\n%s\n\n', iq, features)
self._feature_correct(features)
self._feature_attention(features)
self._feature_receipts(features)

View file

@ -18,7 +18,7 @@ log = logging.getLogger(__name__)
import curses import curses
from . import ChatTab, Tab from . basetabs import OneToOneTab, Tab
import common import common
import fixes import fixes
@ -30,7 +30,7 @@ from decorators import refresh_wrapper
from roster import roster from roster import roster
from theming import get_theme, dump_tuple from theming import get_theme, dump_tuple
class ConversationTab(ChatTab): class ConversationTab(OneToOneTab):
""" """
The tab containg a normal conversation (not from a MUC) The tab containg a normal conversation (not from a MUC)
Must not be instantiated, use Static or Dynamic version only. Must not be instantiated, use Static or Dynamic version only.
@ -40,7 +40,7 @@ class ConversationTab(ChatTab):
additional_informations = {} additional_informations = {}
message_type = 'chat' message_type = 'chat'
def __init__(self, jid): def __init__(self, jid):
ChatTab.__init__(self, jid) OneToOneTab.__init__(self, jid)
self.nick = None self.nick = None
self.nick_sent = False self.nick_sent = False
self.state = 'normal' self.state = 'normal'
@ -49,7 +49,6 @@ class ConversationTab(ChatTab):
self._text_buffer.add_window(self.text_win) self._text_buffer.add_window(self.text_win)
self.upper_bar = windows.ConversationStatusMessageWin() self.upper_bar = windows.ConversationStatusMessageWin()
self.input = windows.MessageInput() self.input = windows.MessageInput()
self.check_attention()
# keys # keys
self.key_func['^I'] = self.completion self.key_func['^I'] = self.completion
# commands # commands
@ -142,6 +141,8 @@ class ConversationTab(ChatTab):
typ=1) typ=1)
self.last_sent_message = msg self.last_sent_message = msg
if self.remote_supports_receipts:
msg._add_receipt = True
msg.send() msg.send()
self.cancel_paused_delay() self.cancel_paused_delay()
self.text_win.refresh() self.text_win.refresh()
@ -208,32 +209,6 @@ class ConversationTab(ChatTab):
self._text_buffer.add_message("\x19%(info_col)s}No information available\x19o" % {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}) self._text_buffer.add_message("\x19%(info_col)s}No information available\x19o" % {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)})
return True return True
def command_attention(self, message=''):
if message is not '':
self.command_say(message, attention=True)
else:
msg = self.core.xmpp.make_message(self.get_dest_jid())
msg['type'] = 'chat'
msg['attention'] = True
msg.send()
def check_attention(self):
self.core.xmpp.plugin['xep_0030'].get_info(
jid=self.get_dest_jid(), block=False, timeout=5,
callback=self.on_attention_checked)
def on_attention_checked(self, iq):
if 'urn:xmpp:attention:0' in iq['disco_info'].get_features():
self.core.information('Attention is supported', 'Info')
self.remote_supports_attention = True
self.commands['attention'] = (self.command_attention,
_('Usage: /attention [message]\nAttention: Require'
' the attention of the contact. Can also send a '
'message along with the attention.'), None)
else:
self.remote_supports_attention = False
def command_unquery(self, arg): def command_unquery(self, arg):
self.core.close_tab() self.core.close_tab()
@ -416,6 +391,7 @@ class DynamicConversationTab(ConversationTab):
self.name, self.name,
resource) resource)
self.add_message(message, typ=0) self.add_message(message, typ=0)
self.check_features()
def unlock_command(self, arg=None): def unlock_command(self, arg=None):
self.unlock() self.unlock()

View file

@ -17,7 +17,7 @@ log = logging.getLogger(__name__)
import curses import curses
from . import ChatTab, MucTab, Tab from . import OneToOneTab, MucTab, Tab
import fixes import fixes
import windows import windows
@ -28,7 +28,7 @@ from decorators import refresh_wrapper
from logger import logger from logger import logger
from theming import get_theme, dump_tuple from theming import get_theme, dump_tuple
class PrivateTab(ChatTab): class PrivateTab(OneToOneTab):
""" """
The tab containg a private conversation (someone from a MUC) The tab containg a private conversation (someone from a MUC)
""" """
@ -37,14 +37,13 @@ class PrivateTab(ChatTab):
additional_informations = {} additional_informations = {}
plugin_keys = {} plugin_keys = {}
def __init__(self, name, nick): def __init__(self, name, nick):
ChatTab.__init__(self, name) OneToOneTab.__init__(self, name)
self.own_nick = nick self.own_nick = nick
self.name = name self.name = name
self.text_win = windows.TextWin() self.text_win = windows.TextWin()
self._text_buffer.add_window(self.text_win) self._text_buffer.add_window(self.text_win)
self.info_header = windows.PrivateInfoWin() self.info_header = windows.PrivateInfoWin()
self.input = windows.MessageInput() self.input = windows.MessageInput()
self.check_attention()
# keys # keys
self.key_func['^I'] = self.completion self.key_func['^I'] = self.completion
# commands # commands
@ -68,6 +67,9 @@ class PrivateTab(ChatTab):
def general_jid(self): def general_jid(self):
return self.name return self.name
def get_dest_jid(self):
return self.name
@property @property
def nick(self): def nick(self):
return self.get_nick() return self.get_nick()
@ -172,36 +174,13 @@ class PrivateTab(ChatTab):
typ=1) typ=1)
self.last_sent_message = msg self.last_sent_message = msg
if self.remote_supports_receipts:
msg._add_receipt = True
msg.send() msg.send()
self.cancel_paused_delay() self.cancel_paused_delay()
self.text_win.refresh() self.text_win.refresh()
self.input.refresh() self.input.refresh()
def command_attention(self, message=''):
if message is not '':
self.command_say(message, attention=True)
else:
msg = self.core.xmpp.make_message(self.name)
msg['type'] = 'chat'
msg['attention'] = True
msg.send()
def check_attention(self):
self.core.xmpp.plugin['xep_0030'].get_info(jid=self.name, block=False, timeout=5, callback=self.on_attention_checked)
def on_attention_checked(self, iq):
if 'urn:xmpp:attention:0' in iq['disco_info'].get_features():
self.core.information('Attention is supported', 'Info')
self.remote_supports_attention = True
self.commands['attention'] = (
self.command_attention,
_('Usage: /attention [message]\nAttention:'
'Require the attention of the contact. Can'
' also send a message along with the attention.'),
None)
else:
self.remote_supports_attention = False
def command_unquery(self, arg): def command_unquery(self, arg):
""" """
/unquery /unquery