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:
parent
916416a019
commit
97ef9d7fb9
7 changed files with 99 additions and 180 deletions
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
97
src/fixes.py
97
src/fixes.py
|
@ -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',
|
return stanza
|
||||||
cached=True):
|
|
||||||
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()
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 don’t 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 that’s None, then no paused chatstate was sent recently
|
# if that’s None, then no paused chatstate was sent recently
|
||||||
# if that’s a weakref returning None, then a paused chatstate was sent
|
# if that’s 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 don’t 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue