summaryrefslogtreecommitdiff
path: root/src/tabs
diff options
context:
space:
mode:
Diffstat (limited to 'src/tabs')
-rw-r--r--src/tabs/__init__.py1
-rw-r--r--src/tabs/adhoc_commands_list.py6
-rw-r--r--src/tabs/basetabs.py124
-rw-r--r--src/tabs/bookmarkstab.py145
-rw-r--r--src/tabs/conversationtab.py73
-rw-r--r--src/tabs/listtab.py6
-rw-r--r--src/tabs/muclisttab.py8
-rw-r--r--src/tabs/muctab.py674
-rw-r--r--src/tabs/privatetab.py44
-rw-r--r--src/tabs/rostertab.py546
-rw-r--r--src/tabs/xmltab.py225
11 files changed, 1263 insertions, 589 deletions
diff --git a/src/tabs/__init__.py b/src/tabs/__init__.py
index eaf41a2f..d0a881a6 100644
--- a/src/tabs/__init__.py
+++ b/src/tabs/__init__.py
@@ -10,3 +10,4 @@ from . listtab import ListTab
from . muclisttab import MucListTab
from . adhoc_commands_list import AdhocCommandsListTab
from . data_forms import DataFormsTab
+from . bookmarkstab import BookmarksTab
diff --git a/src/tabs/adhoc_commands_list.py b/src/tabs/adhoc_commands_list.py
index 7f5abf6a..10ebf22b 100644
--- a/src/tabs/adhoc_commands_list.py
+++ b/src/tabs/adhoc_commands_list.py
@@ -4,8 +4,6 @@ select one of them and start executing it, or just close the tab and do
nothing.
"""
-from gettext import gettext as _
-
import logging
log = logging.getLogger(__name__)
@@ -20,7 +18,7 @@ class AdhocCommandsListTab(ListTab):
def __init__(self, jid):
ListTab.__init__(self, jid.full,
"“Enter”: execute selected command.",
- _('Ad-hoc commands of JID %s (Loading)') % jid,
+ 'Ad-hoc commands of JID %s (Loading)' % jid,
(('Node', 0), ('Description', 1)))
self.key_func['^M'] = self.execute_selected_command
@@ -50,7 +48,7 @@ class AdhocCommandsListTab(ListTab):
yield item
items = [(item['node'], item['name'] or '', item['jid']) for item in get_items()]
self.listview.set_lines(items)
- self.info_header.message = _('Ad-hoc commands of JID %s') % self.name
+ self.info_header.message = 'Ad-hoc commands of JID %s' % self.name
if self.core.current_tab() is self:
self.refresh()
else:
diff --git a/src/tabs/basetabs.py b/src/tabs/basetabs.py
index 0a55640c..30ddf239 100644
--- a/src/tabs/basetabs.py
+++ b/src/tabs/basetabs.py
@@ -13,8 +13,6 @@ This module also defines ChatTabs, the parent class for all tabs
revolving around chats.
"""
-from gettext import gettext as _
-
import logging
log = logging.getLogger(__name__)
@@ -35,7 +33,7 @@ from decorators import refresh_wrapper
from logger import logger
from text_buffer import TextBuffer
from theming import get_theme, dump_tuple
-
+from decorators import command_args_parser
# getters for tab colors (lambdas, so that they are dynamic)
STATE_COLORS = {
@@ -254,7 +252,6 @@ class Tab(object):
return False # There's no completion function
else:
return command[2](the_input)
- return True
return False
def execute_command(self, provided_text):
@@ -282,14 +279,15 @@ class Tab(object):
if self.missing_command_callback is not None:
error_handled = self.missing_command_callback(low)
if not error_handled:
- self.core.information(_("Unknown command (%s)") %
- (command),
- _('Error'))
+ self.core.information("Unknown command (%s)" %
+ (command),
+ 'Error')
if command in ('correct', 'say'): # hack
arg = xhtml.convert_simple_to_full_colors(arg)
else:
arg = xhtml.clean_text_simple(arg)
if func:
+ self.input.reset_completion()
func(arg)
return True
else:
@@ -455,16 +453,16 @@ class ChatTab(Tab):
self.key_func['M-/'] = self.last_words_completion
self.key_func['^M'] = self.on_enter
self.register_command('say', self.command_say,
- usage=_('<message>'),
- shortdesc=_('Send the message.'))
+ usage='<message>',
+ shortdesc='Send the message.')
self.register_command('xhtml', self.command_xhtml,
- usage=_('<custom xhtml>'),
- shortdesc=_('Send custom XHTML.'))
+ usage='<custom xhtml>',
+ shortdesc='Send custom XHTML.')
self.register_command('clear', self.command_clear,
- shortdesc=_('Clear the current buffer.'))
+ shortdesc='Clear the current buffer.')
self.register_command('correct', self.command_correct,
- desc=_('Fix the last message with whatever you want.'),
- shortdesc=_('Correct the last message.'),
+ desc='Fix the last message with whatever you want.',
+ shortdesc='Correct the last message.',
completion=self.completion_correct)
self.chat_state = None
self.update_commands()
@@ -492,7 +490,7 @@ class ChatTab(Tab):
"""
name = safeJID(self.name).bare
if not logger.log_message(name, nickname, txt, date=time, typ=typ):
- self.core.information(_('Unable to write in the log file'), 'Error')
+ self.core.information('Unable to write in the log file', 'Error')
def add_message(self, txt, time=None, nickname=None, forced_user=None,
nick_color=None, identifier=None, jid=None, history=None,
@@ -544,11 +542,12 @@ class ChatTab(Tab):
self.command_say(xhtml.convert_simple_to_full_colors(txt))
self.cancel_paused_delay()
- def command_xhtml(self, arg):
+ @command_args_parser.raw
+ def command_xhtml(self, xhtml):
""""
/xhtml <custom xhtml>
"""
- message = self.generate_xhtml_message(arg)
+ message = self.generate_xhtml_message(xhtml)
if message:
message.send()
@@ -573,7 +572,7 @@ class ChatTab(Tab):
return self.name
@refresh_wrapper.always
- def command_clear(self, args):
+ def command_clear(self, ignored):
"""
/clear
"""
@@ -637,6 +636,7 @@ class ChatTab(Tab):
self.core.remove_timed_event(self.timed_event_paused)
self.timed_event_paused = None
+ @command_args_parser.raw
def command_correct(self, line):
"""
/correct <fixed message>
@@ -645,7 +645,7 @@ class ChatTab(Tab):
self.core.command_help('correct')
return
if not self.last_sent_message:
- self.core.information(_('There is no message to correct.'))
+ self.core.information('There is no message to correct.')
return
self.command_say(line, correct=True)
@@ -672,6 +672,7 @@ class ChatTab(Tab):
if self.text_win.pos != 0:
self.state = 'scrolled'
+ @command_args_parser.raw
def command_say(self, line, correct=False):
pass
@@ -707,20 +708,67 @@ class OneToOneTab(ChatTab):
# 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_wants_chatstates = None
self.remote_supports_attention = True
self.remote_supports_receipts = True
self.check_features()
- def ack_message(self, msg_id):
+ @property
+ def remote_wants_chatstates(self):
+ return self._remote_wants_chatstates
+
+ @remote_wants_chatstates.setter
+ def remote_wants_chatstates(self, value):
+ old_value = self._remote_wants_chatstates
+ self._remote_wants_chatstates = value
+ if (old_value is None and value != None) or \
+ (old_value != value and value != None):
+ ok = get_theme().CHAR_OK
+ nope = get_theme().CHAR_EMPTY
+ support = ok if value else nope
+ if value:
+ msg = '\x19%s}Contact supports chat states [%s].'
+ else:
+ msg = '\x19%s}Contact does not support chat states [%s].'
+ color = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
+ msg = msg % (color, support)
+ self.add_message(msg, typ=0)
+ self.core.refresh_window()
+
+ def ack_message(self, msg_id, msg_jid):
"""
Ack a message
"""
- new_msg = self._text_buffer.ack_message(msg_id)
+ new_msg = self._text_buffer.ack_message(msg_id, msg_jid)
if new_msg:
self.text_win.modify_message(msg_id, new_msg)
self.core.refresh_window()
+ def nack_message(self, error, msg_id, msg_jid):
+ """
+ Ack a message
+ """
+ new_msg = self._text_buffer.nack_message(error, msg_id, msg_jid)
+ if new_msg:
+ self.text_win.modify_message(msg_id, new_msg)
+ self.core.refresh_window()
+ return True
+ return False
+
+ @command_args_parser.raw
+ def command_xhtml(self, xhtml_data):
+ message = self.generate_xhtml_message(xhtml_data)
+ if message:
+ if self.remote_supports_receipts:
+ message._add_receipt = True
+ if self.remote_wants_chatstates:
+ message['chat_sate'] = 'active'
+ message.send()
+ body = xhtml.xhtml_to_poezio_colors(xhtml_data, force=True)
+ self._text_buffer.add_message(body, nickname=self.core.own_nick,
+ identifier=message['id'],)
+ self.refresh()
+
def check_features(self):
"check the features supported by the other party"
if safeJID(self.get_dest_jid()).resource:
@@ -728,8 +776,9 @@ class OneToOneTab(ChatTab):
jid=self.get_dest_jid(), timeout=5,
callback=self.features_checked)
- def command_attention(self, message=''):
- "/attention [message]"
+ @command_args_parser.raw
+ def command_attention(self, message):
+ """/attention [message]"""
if message is not '':
self.command_say(message, attention=True)
else:
@@ -738,6 +787,7 @@ class OneToOneTab(ChatTab):
msg['attention'] = True
msg.send()
+ @command_args_parser.raw
def command_say(self, line, correct=False, attention=False):
pass
@@ -746,11 +796,11 @@ class OneToOneTab(ChatTab):
return False
if command_name == 'correct':
- feature = _('message correction')
+ feature = 'message correction'
elif command_name == 'attention':
- feature = _('attention requests')
- msg = _('%s does not support %s, therefore the /%s '
- 'command is currently disabled in this tab.')
+ feature = 'attention requests'
+ msg = ('%s does not support %s, therefore the /%s '
+ 'command is currently disabled in this tab.')
msg = msg % (self.name, feature, command_name)
self.core.information(msg, 'Info')
return True
@@ -760,11 +810,11 @@ class OneToOneTab(ChatTab):
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.'))
+ 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
return self.remote_supports_attention
@@ -776,8 +826,8 @@ class OneToOneTab(ChatTab):
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.'),
+ desc='Fix the last message with whatever you want.',
+ shortdesc='Correct the last message.',
completion=self.completion_correct)
return 'correct' in self.commands
@@ -814,8 +864,8 @@ class OneToOneTab(ChatTab):
attention = ok if attention else nope
receipts = ok if receipts else nope
- msg = _('\x19%s}Contact supports: correction [%s], '
- 'attention [%s], receipts [%s].')
+ msg = ('\x19%s}Contact supports: correction [%s], '
+ 'attention [%s], receipts [%s].')
color = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
msg = msg % (color, correct, attention, receipts)
self.add_message(msg, typ=0)
diff --git a/src/tabs/bookmarkstab.py b/src/tabs/bookmarkstab.py
new file mode 100644
index 00000000..7f5069ea
--- /dev/null
+++ b/src/tabs/bookmarkstab.py
@@ -0,0 +1,145 @@
+"""
+Defines the data-forms Tab
+"""
+
+import logging
+log = logging.getLogger(__name__)
+
+import windows
+from bookmarks import Bookmark, BookmarkList, stanza_storage
+from tabs import Tab
+from common import safeJID
+
+
+class BookmarksTab(Tab):
+ """
+ A tab displaying lines of bookmarks, each bookmark having
+ a 4 widgets to set the jid/password/autojoin/storage method
+ """
+ plugin_commands = {}
+ def __init__(self, bookmarks: BookmarkList):
+ Tab.__init__(self)
+ self.name = "Bookmarks"
+ self.bookmarks = bookmarks
+ self.new_bookmarks = []
+ self.removed_bookmarks = []
+ self.header_win = windows.ColumnHeaderWin(('room@server/nickname',
+ 'password',
+ 'autojoin',
+ 'storage'))
+ self.bookmarks_win = windows.BookmarksWin(self.bookmarks,
+ self.height-4,
+ self.width, 1, 0)
+ self.help_win = windows.HelpText('Ctrl+Y: save, Ctrl+G: cancel, '
+ '↑↓: change lines, tab: change '
+ 'column, M-a: add bookmark, C-k'
+ ': delete bookmark')
+ self.info_header = windows.BookmarksInfoWin()
+ self.key_func['KEY_UP'] = self.bookmarks_win.go_to_previous_line_input
+ self.key_func['KEY_DOWN'] = self.bookmarks_win.go_to_next_line_input
+ self.key_func['^I'] = self.bookmarks_win.go_to_next_horizontal_input
+ self.key_func['^G'] = self.on_cancel
+ self.key_func['^Y'] = self.on_save
+ self.key_func['M-a'] = self.add_bookmark
+ self.key_func['^K'] = self.del_bookmark
+ self.resize()
+ self.update_commands()
+
+ def add_bookmark(self):
+ new_bookmark = Bookmark(safeJID('room@example.tld/nick'), method='local')
+ self.new_bookmarks.append(new_bookmark)
+ self.bookmarks_win.add_bookmark(new_bookmark)
+
+ def del_bookmark(self):
+ current = self.bookmarks_win.del_current_bookmark()
+ if current in self.new_bookmarks:
+ self.new_bookmarks.remove(current)
+ else:
+ self.removed_bookmarks.append(current)
+
+ def on_cancel(self):
+ self.core.close_tab()
+ return True
+
+ def on_save(self):
+ self.bookmarks_win.save()
+ if find_duplicates(self.new_bookmarks):
+ self.core.information('Duplicate bookmarks in list (saving aborted)', 'Error')
+ return
+ for bm in self.new_bookmarks:
+ if safeJID(bm.jid):
+ if not self.bookmarks[bm.jid]:
+ self.bookmarks.append(bm)
+ else:
+ self.core.information('Invalid JID for bookmark: %s/%s' % (bm.jid, bm.nick), 'Error')
+ return
+
+ for bm in self.removed_bookmarks:
+ if bm in self.bookmarks:
+ self.bookmarks.remove(bm)
+
+ def send_cb(success):
+ if success:
+ self.core.information('Bookmarks saved.', 'Info')
+ else:
+ self.core.information('Remote bookmarks not saved.', 'Error')
+ log.debug('alerte %s', str(stanza_storage(self.bookmarks.bookmarks)))
+ self.bookmarks.save(self.core.xmpp, callback=send_cb)
+ self.core.close_tab()
+ return True
+
+ def on_input(self, key, raw=False):
+ if key in self.key_func:
+ res = self.key_func[key]()
+ if res:
+ return res
+ self.bookmarks_win.refresh_current_input()
+ else:
+ self.bookmarks_win.on_input(key)
+
+ def resize(self):
+ self.need_resize = False
+ self.header_win.resize_columns({
+ 'room@server/nickname': self.width//3,
+ 'password': self.width//3,
+ 'autojoin': self.width//6,
+ 'storage': self.width//6
+ })
+ info_height = self.core.information_win_size
+ tab_height = Tab.tab_win_height()
+ self.header_win.resize(1, self.width, 0, 0)
+ self.bookmarks_win.resize(self.height - 3 - tab_height - info_height,
+ self.width, 1, 0)
+ self.help_win.resize(1, self.width, self.height - 1, 0)
+ self.info_header.resize(1, self.width,
+ self.height - 2 - tab_height - info_height, 0)
+
+ def on_info_win_size_changed(self):
+ if self.core.information_win_size >= self.height - 3:
+ return
+ info_height = self.core.information_win_size
+ tab_height = Tab.tab_win_height()
+ self.bookmarks_win.resize(self.height - 3 - tab_height - info_height,
+ self.width, 1, 0)
+ self.info_header.resize(1, self.width,
+ self.height - 2 - tab_height - info_height, 0)
+
+ def refresh(self):
+ if self.need_resize:
+ self.resize()
+ self.header_win.refresh()
+ self.refresh_tab_win()
+ self.help_win.refresh()
+ self.info_header.refresh(self.bookmarks.preferred)
+ self.info_win.refresh()
+ self.bookmarks_win.refresh()
+
+
+def find_duplicates(bm_list):
+ jids = set()
+ for bookmark in bm_list:
+ if bookmark.jid in jids:
+ return True
+ jids.add(bookmark.jid)
+ return False
+
diff --git a/src/tabs/conversationtab.py b/src/tabs/conversationtab.py
index 52c503d7..1d8c60a4 100644
--- a/src/tabs/conversationtab.py
+++ b/src/tabs/conversationtab.py
@@ -11,8 +11,6 @@ There are two different instances of a ConversationTab:
the time.
"""
-from gettext import gettext as _
-
import logging
log = logging.getLogger(__name__)
@@ -29,6 +27,7 @@ from config import config
from decorators import refresh_wrapper
from roster import roster
from theming import get_theme, dump_tuple
+from decorators import command_args_parser
class ConversationTab(OneToOneTab):
"""
@@ -53,18 +52,18 @@ class ConversationTab(OneToOneTab):
self.key_func['^I'] = self.completion
# commands
self.register_command('unquery', self.command_unquery,
- shortdesc=_('Close the tab.'))
+ shortdesc='Close the tab.')
self.register_command('close', self.command_unquery,
- shortdesc=_('Close the tab.'))
+ shortdesc='Close the tab.')
self.register_command('version', self.command_version,
- desc=_('Get the software version of the current interlocutor (usually its XMPP client and Operating System).'),
- shortdesc=_('Get the software version of the user.'))
+ desc='Get the software version of the current interlocutor (usually its XMPP client and Operating System).',
+ shortdesc='Get the software version of the user.')
self.register_command('info', self.command_info,
- shortdesc=_('Get the status of the contact.'))
+ shortdesc='Get the status of the contact.')
self.register_command('last_activity', self.command_last_activity,
- usage=_('[jid]'),
- desc=_('Get the last activity of the given or the current contact.'),
- shortdesc=_('Get the activity.'),
+ usage='[jid]',
+ desc='Get the last activity of the given or the current contact.',
+ shortdesc='Get the activity.',
completion=self.core.completion_last_activity)
self.resize()
self.update_commands()
@@ -88,6 +87,7 @@ class ConversationTab(OneToOneTab):
def completion(self):
self.complete_commands(self.input)
+ @command_args_parser.raw
def command_say(self, line, attention=False, correct=False):
msg = self.core.xmpp.make_message(self.get_dest_jid())
msg['type'] = 'chat'
@@ -149,19 +149,13 @@ class ConversationTab(OneToOneTab):
self.text_win.refresh()
self.input.refresh()
- def command_xhtml(self, arg):
- message = self.generate_xhtml_message(arg)
- if message:
- message.send()
- self.core.add_message_to_text_buffer(self._text_buffer, message['body'], None, self.core.own_nick)
- self.refresh()
-
- def command_last_activity(self, arg):
+ @command_args_parser.quoted(0, 1)
+ def command_last_activity(self, args):
"""
- /activity [jid]
+ /last_activity [jid]
"""
- if arg.strip():
- return self.core.command_last_activity(arg)
+ if args and args[0]:
+ return self.core.command_last_activity(args[0])
def callback(iq):
if iq['type'] != 'result':
@@ -188,10 +182,11 @@ class ConversationTab(OneToOneTab):
self.add_message(msg)
self.core.refresh_window()
- self.core.xmpp.plugin['xep_0012'].get_last_activity(self.general_jid, callback=callback)
+ self.core.xmpp.plugin['xep_0012'].get_last_activity(self.get_dest_jid(), callback=callback)
@refresh_wrapper.conditional
- def command_info(self, arg):
+ @command_args_parser.ignored
+ def command_info(self):
contact = roster[self.get_dest_jid()]
jid = safeJID(self.get_dest_jid())
if contact:
@@ -202,7 +197,7 @@ class ConversationTab(OneToOneTab):
else:
resource = None
if resource:
- status = (_('Status: %s') % resource.status) if resource.status else ''
+ status = ('Status: %s' % resource.status) if resource.status else ''
self._text_buffer.add_message("\x19%(info_col)s}Show: %(show)s, %(status)s\x19o" % {
'show': resource.show or 'available', 'status': status, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)})
return True
@@ -210,23 +205,25 @@ class ConversationTab(OneToOneTab):
self._text_buffer.add_message("\x19%(info_col)s}No information available\x19o" % {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)})
return True
- def command_unquery(self, arg):
+ @command_args_parser.ignored
+ def command_unquery(self):
self.core.close_tab()
- def command_version(self, arg):
+ @command_args_parser.quoted(0, 1)
+ def command_version(self, args):
"""
- /version
+ /version [jid]
"""
def callback(res):
if not res:
return self.core.information('Could not get the software version from %s' % (jid,), 'Warning')
version = '%s is running %s version %s on %s' % (jid,
- res.get('name') or _('an unknown software'),
- res.get('version') or _('unknown'),
- res.get('os') or _('an unknown platform'))
+ res.get('name') or 'an unknown software',
+ res.get('version') or 'unknown',
+ res.get('os') or 'an unknown platform')
self.core.information(version, 'Info')
- if arg:
- return self.core.command_version(arg)
+ if args:
+ return self.core.command_version(args[0])
jid = safeJID(self.name)
if not jid.resource:
if jid in roster:
@@ -381,7 +378,7 @@ class DynamicConversationTab(ConversationTab):
self.info_header = windows.DynamicConversationInfoWin()
ConversationTab.__init__(self, jid)
self.register_command('unlock', self.unlock_command,
- shortdesc=_('Unlock the conversation from a particular resource.'))
+ shortdesc='Unlock the conversation from a particular resource.')
def lock(self, resource):
"""
@@ -393,8 +390,8 @@ class DynamicConversationTab(ConversationTab):
info = '\x19%s}' % dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
jid_c = '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID)
- message = _('%(info)sConversation locked to '
- '%(jid_c)s%(jid)s/%(resource)s%(info)s.') % {
+ message = ('%(info)sConversation locked to '
+ '%(jid_c)s%(jid)s/%(resource)s%(info)s.') % {
'info': info,
'jid_c': jid_c,
'jid': self.name,
@@ -418,14 +415,14 @@ class DynamicConversationTab(ConversationTab):
jid_c = '\x19%s}' % dump_tuple(get_theme().COLOR_MUC_JID)
if from_:
- message = _('%(info)sConversation unlocked (received activity'
- ' from %(jid_c)s%(jid)s%(info)s).') % {
+ message = ('%(info)sConversation unlocked (received activity'
+ ' from %(jid_c)s%(jid)s%(info)s).') % {
'info': info,
'jid_c': jid_c,
'jid': from_}
self.add_message(message, typ=0)
else:
- message = _('%sConversation unlocked.') % info
+ message = '%sConversation unlocked.' % info
self.add_message(message, typ=0)
def get_dest_jid(self):
diff --git a/src/tabs/listtab.py b/src/tabs/listtab.py
index c5aab5eb..7021c8e3 100644
--- a/src/tabs/listtab.py
+++ b/src/tabs/listtab.py
@@ -4,8 +4,6 @@ sortable list. It should be inherited, to actually provide methods that
insert items in the list, and that lets the user interact with them.
"""
-from gettext import gettext as _
-
import logging
log = logging.getLogger(__name__)
@@ -52,7 +50,7 @@ class ListTab(Tab):
self.key_func['KEY_RIGHT'] = self.list_header.sel_column_right
self.key_func[' '] = self.sort_by
self.register_command('close', self.close,
- shortdesc=_('Close this tab.'))
+ shortdesc='Close this tab.')
self.resize()
self.update_keys()
self.update_commands()
@@ -121,7 +119,7 @@ class ListTab(Tab):
"""
If there's an error (retrieving the values etc)
"""
- self._error_message = _('Error: %(code)s - %(msg)s: %(body)s') % {'msg':msg, 'body':body, 'code':code}
+ self._error_message = 'Error: %(code)s - %(msg)s: %(body)s' % {'msg':msg, 'body':body, 'code':code}
self.info_header.message = self._error_message
self.info_header.refresh()
curses.doupdate()
diff --git a/src/tabs/muclisttab.py b/src/tabs/muclisttab.py
index 55d5c2bd..c26fb268 100644
--- a/src/tabs/muclisttab.py
+++ b/src/tabs/muclisttab.py
@@ -4,8 +4,6 @@ A MucListTab is a tab listing the rooms on a conference server.
It has no functionnality except scrolling the list, and allowing the
user to join the rooms.
"""
-from gettext import gettext as _
-
import logging
log = logging.getLogger(__name__)
@@ -22,9 +20,9 @@ class MucListTab(ListTab):
plugin_keys = {}
def __init__(self, server):
- ListTab.__init__(self, server,
+ ListTab.__init__(self, server.full,
"“j”: join room.",
- _('Chatroom list on server %s (Loading)') % server,
+ 'Chatroom list on server %s (Loading)' % server,
(('node-part', 0), ('name', 2), ('users', 3)))
self.key_func['j'] = self.join_selected
self.key_func['J'] = self.join_selected_no_focus
@@ -56,7 +54,7 @@ class MucListTab(ListTab):
item[0],
item[2] or '', '') for item in get_items()]
self.listview.set_lines(items)
- self.info_header.message = _('Chatroom list on server %s') % self.name
+ self.info_header.message = 'Chatroom list on server %s' % self.name
if self.core.current_tab() is self:
self.refresh()
else:
diff --git a/src/tabs/muctab.py b/src/tabs/muctab.py
index 8ac9b7e2..d4b13258 100644
--- a/src/tabs/muctab.py
+++ b/src/tabs/muctab.py
@@ -7,8 +7,6 @@ It keeps track of many things such as part/joins, maintains an
user list, and updates private tabs when necessary.
"""
-from gettext import gettext as _
-
import logging
log = logging.getLogger(__name__)
@@ -29,7 +27,7 @@ import windows
import xhtml
from common import safeJID
from config import config
-from decorators import refresh_wrapper
+from decorators import refresh_wrapper, command_args_parser
from logger import logger
from roster import roster
from theming import get_theme, dump_tuple
@@ -37,11 +35,11 @@ from user import User
SHOW_NAME = {
- 'dnd': _('busy'),
- 'away': _('away'),
- 'xa': _('not available'),
- 'chat': _('chatty'),
- '': _('available')
+ 'dnd': 'busy',
+ 'away': 'away',
+ 'xa': 'not available',
+ 'chat': 'chatty',
+ '': 'available'
}
NS_MUC_USER = 'http://jabber.org/protocol/muc#user'
@@ -55,13 +53,14 @@ class MucTab(ChatTab):
message_type = 'groupchat'
plugin_commands = {}
plugin_keys = {}
- def __init__(self, jid, nick):
+ def __init__(self, jid, nick, password=None):
self.joined = False
ChatTab.__init__(self, jid)
if self.joined == False:
self._state = 'disconnected'
self.own_nick = nick
self.name = jid
+ self.password = password
self.users = []
self.privates = [] # private conversations
self.topic = ''
@@ -88,106 +87,112 @@ class MucTab(ChatTab):
self.key_func['M-p'] = self.go_to_prev_hl
# commands
self.register_command('ignore', self.command_ignore,
- usage=_('<nickname>'),
- desc=_('Ignore a specified nickname.'),
- shortdesc=_('Ignore someone'),
+ usage='<nickname>',
+ desc='Ignore a specified nickname.',
+ shortdesc='Ignore someone',
completion=self.completion_ignore)
self.register_command('unignore', self.command_unignore,
- usage=_('<nickname>'),
- desc=_('Remove the specified nickname from the ignore list.'),
- shortdesc=_('Unignore someone.'),
+ usage='<nickname>',
+ desc='Remove the specified nickname from the ignore list.',
+ shortdesc='Unignore someone.',
completion=self.completion_unignore)
self.register_command('kick', self.command_kick,
- usage=_('<nick> [reason]'),
- desc=_('Kick the user with the specified nickname.'
- ' You also can give an optional reason.'),
- shortdesc=_('Kick someone.'),
+ usage='<nick> [reason]',
+ desc='Kick the user with the specified nickname.'
+ ' You also can give an optional reason.',
+ shortdesc='Kick someone.',
completion=self.completion_quoted)
self.register_command('ban', self.command_ban,
- usage=_('<nick> [reason]'),
- desc=_('Ban the user with the specified nickname.'
- ' You also can give an optional reason.'),
+ usage='<nick> [reason]',
+ desc='Ban the user with the specified nickname.'
+ ' You also can give an optional reason.',
shortdesc='Ban someone',
completion=self.completion_quoted)
self.register_command('role', self.command_role,
- usage=_('<nick> <role> [reason]'),
- desc=_('Set the role of an user. Roles can be:'
- ' none, visitor, participant, moderator.'
- ' You also can give an optional reason.'),
- shortdesc=_('Set the role of an user.'),
+ usage='<nick> <role> [reason]',
+ desc='Set the role of an user. Roles can be:'
+ ' none, visitor, participant, moderator.'
+ ' You also can give an optional reason.',
+ shortdesc='Set the role of an user.',
completion=self.completion_role)
self.register_command('affiliation', self.command_affiliation,
- usage=_('<nick or jid> <affiliation>'),
- desc=_('Set the affiliation of an user. Affiliations can be:'
- ' outcast, none, member, admin, owner.'),
- shortdesc=_('Set the affiliation of an user.'),
+ usage='<nick or jid> <affiliation>',
+ desc='Set the affiliation of an user. Affiliations can be:'
+ ' outcast, none, member, admin, owner.',
+ shortdesc='Set the affiliation of an user.',
completion=self.completion_affiliation)
self.register_command('topic', self.command_topic,
- usage=_('<subject>'),
- desc=_('Change the subject of the room.'),
- shortdesc=_('Change the subject.'),
+ usage='<subject>',
+ desc='Change the subject of the room.',
+ shortdesc='Change the subject.',
completion=self.completion_topic)
self.register_command('query', self.command_query,
- usage=_('<nick> [message]'),
- desc=_('Open a private conversation with <nick>. This nick'
- ' has to be present in the room you\'re currently in.'
- ' If you specified a message after the nickname, it '
- 'will immediately be sent to this user.'),
- shortdesc=_('Query an user.'),
+ usage='<nick> [message]',
+ desc='Open a private conversation with <nick>. This nick'
+ ' has to be present in the room you\'re currently in.'
+ ' If you specified a message after the nickname, it '
+ 'will immediately be sent to this user.',
+ shortdesc='Query an user.',
completion=self.completion_quoted)
self.register_command('part', self.command_part,
- usage=_('[message]'),
- desc=_('Disconnect from a room. You can'
- ' specify an optional message.'),
- shortdesc=_('Leave the room.'))
+ usage='[message]',
+ desc='Disconnect from a room. You can'
+ ' specify an optional message.',
+ shortdesc='Leave the room.')
self.register_command('close', self.command_close,
- usage=_('[message]'),
- desc=_('Disconnect from a room and close the tab.'
- ' You can specify an optional message if '
- 'you are still connected.'),
- shortdesc=_('Close the tab.'))
+ usage='[message]',
+ desc='Disconnect from a room and close the tab.'
+ ' You can specify an optional message if '
+ 'you are still connected.',
+ shortdesc='Close the tab.')
self.register_command('nick', self.command_nick,
- usage=_('<nickname>'),
- desc=_('Change your nickname in the current room.'),
- shortdesc=_('Change your nickname.'),
+ usage='<nickname>',
+ desc='Change your nickname in the current room.',
+ shortdesc='Change your nickname.',
completion=self.completion_nick)
self.register_command('recolor', self.command_recolor,
- usage=_('[random]'),
- desc=_('Re-assign a color to all participants of the'
- ' current room, based on the last time they talked.'
- ' Use this if the participants currently talking '
- 'have too many identical colors. Use /recolor random'
- ' for a non-deterministic result.'),
- shortdesc=_('Change the nicks colors.'),
+ usage='[random]',
+ desc='Re-assign a color to all participants of the'
+ ' current room, based on the last time they talked.'
+ ' Use this if the participants currently talking '
+ 'have too many identical colors. Use /recolor random'
+ ' for a non-deterministic result.',
+ shortdesc='Change the nicks colors.',
completion=self.completion_recolor)
+ self.register_command('color', self.command_color,
+ usage='<nick> <color>',
+ desc='Fix a color for a nick. Use "unset" instead of a color'
+ ' to remove the attribution',
+ shortdesc='Fix a color for a nick.',
+ completion=self.completion_color)
self.register_command('cycle', self.command_cycle,
- usage=_('[message]'),
- desc=_('Leave the current room and rejoin it immediately.'),
- shortdesc=_('Leave and re-join the room.'))
+ usage='[message]',
+ desc='Leave the current room and rejoin it immediately.',
+ shortdesc='Leave and re-join the room.')
self.register_command('info', self.command_info,
- usage=_('<nickname>'),
- desc=_('Display some information about the user '
- 'in the MUC: its/his/her role, affiliation,'
- ' status and status message.'),
- shortdesc=_('Show an user\'s infos.'),
+ usage='<nickname>',
+ desc='Display some information about the user '
+ 'in the MUC: its/his/her role, affiliation,'
+ ' status and status message.',
+ shortdesc='Show an user\'s infos.',
completion=self.completion_info)
self.register_command('configure', self.command_configure,
- desc=_('Configure the current room, through a form.'),
- shortdesc=_('Configure the room.'))
+ desc='Configure the current room, through a form.',
+ shortdesc='Configure the room.')
self.register_command('version', self.command_version,
- usage=_('<jid or nick>'),
- desc=_('Get the software version of the given JID'
- ' or nick in room (usually its XMPP client'
- ' and Operating System).'),
- shortdesc=_('Get the software version of a jid.'),
+ usage='<jid or nick>',
+ desc='Get the software version of the given JID'
+ ' or nick in room (usually its XMPP client'
+ ' and Operating System).',
+ shortdesc='Get the software version of a jid.',
completion=self.completion_version)
self.register_command('names', self.command_names,
- desc=_('Get the users in the room with their roles.'),
- shortdesc=_('List the users.'))
+ desc='Get the users in the room with their roles.',
+ shortdesc='List the users.')
self.register_command('invite', self.command_invite,
- desc=_('Invite a contact to this room'),
- usage=_('<jid> [reason]'),
- shortdesc=_('Invite a contact to this room'),
+ desc='Invite a contact to this room',
+ usage='<jid> [reason]',
+ shortdesc='Invite a contact to this room',
completion=self.completion_invite)
if self.core.xmpp.boundjid.server == "gmail.com": #gmail sucks
@@ -263,6 +268,21 @@ class MucTab(ChatTab):
return the_input.new_completion(['random'], 1, '', quotify=False)
return True
+ def completion_color(self, the_input):
+ """Completion for /color"""
+ n = the_input.get_argument_position(quoted=True)
+ if n == 1:
+ userlist = [user.nick for user in self.users]
+ if self.own_nick in userlist:
+ userlist.remove(self.own_nick)
+ return the_input.new_completion(userlist, 1, '', quotify=True)
+ elif n == 2:
+ colors = [i for i in xhtml.colors if i]
+ colors.sort()
+ colors.append('unset')
+ colors.append('random')
+ return the_input.new_completion(colors, 2, '', quotify=False)
+
def completion_ignore(self, the_input):
"""Completion for /ignore"""
userlist = [user.nick for user in self.users]
@@ -302,15 +322,12 @@ class MucTab(ChatTab):
return the_input.new_completion(possible_affiliations, 2, '',
quotify=True)
+ @command_args_parser.quoted(1, 1, [''])
def command_invite(self, args):
"""/invite <jid> [reason]"""
- args = common.shell_split(args)
- if len(args) == 1:
- jid, reason = args[0], ''
- elif len(args) == 2:
- jid, reason = args
- else:
+ if args is None:
return self.core.command_help('invite')
+ jid, reason = args
self.core.command_invite('%s %s "%s"' % (jid, self.name, reason))
def completion_invite(self, the_input):
@@ -329,15 +346,17 @@ class MucTab(ChatTab):
self.user_win.refresh(self.users)
self.input.refresh()
- def command_info(self, arg):
+ @command_args_parser.quoted(1)
+ def command_info(self, args):
"""
/info <nick>
"""
- if not arg:
+ if args is None:
return self.core.command_help('info')
- user = self.get_user_by_name(arg)
+ nick = args[0]
+ user = self.get_user_by_name(nick)
if not user:
- return self.core.information(_("Unknown user: %s") % arg)
+ return self.core.information("Unknown user: %s" % nick)
theme = get_theme()
if user.jid:
user_jid = ' (\x19%s}%s\x19o)' % (
@@ -345,10 +364,10 @@ class MucTab(ChatTab):
user.jid)
else:
user_jid = ''
- info = _('\x19%s}%s\x19o%s: show: \x19%s}%s\x19o, affiliation:'
- ' \x19%s}%s\x19o, role: \x19%s}%s\x19o%s') % (
+ info = ('\x19%s}%s\x19o%s: show: \x19%s}%s\x19o, affiliation:'
+ ' \x19%s}%s\x19o, role: \x19%s}%s\x19o%s') % (
dump_tuple(user.color),
- arg,
+ nick,
user_jid,
dump_tuple(theme.color_show(user.show)),
user.show or 'Available',
@@ -360,19 +379,20 @@ class MucTab(ChatTab):
self.add_message(info, typ=0)
self.core.refresh_window()
- def command_configure(self, arg):
+ @command_args_parser.quoted(0)
+ def command_configure(self, ignored):
"""
/configure
"""
def on_form_received(form):
if not form:
self.core.information(
- _('Could not retrieve the configuration form'),
- _('Error'))
+ 'Could not retrieve the configuration form',
+ 'Error')
return
self.core.open_new_form(form, self.cancel_config, self.send_config)
- form = fixes.get_room_form(self.core.xmpp, self.name, on_form_received)
+ fixes.get_room_form(self.core.xmpp, self.name, on_form_received)
def cancel_config(self, form):
"""
@@ -388,30 +408,53 @@ class MucTab(ChatTab):
muc.configure_room(self.core.xmpp, self.name, form)
self.core.close_tab()
- def command_cycle(self, arg):
+ @command_args_parser.raw
+ def command_cycle(self, msg):
"""/cycle [reason]"""
- self.command_part(arg)
+ self.command_part(msg)
self.disconnect()
self.user_win.pos = 0
self.core.disable_private_tabs(self.name)
self.core.command_join('"/%s"' % self.own_nick)
- def command_recolor(self, arg):
+ @command_args_parser.quoted(0, 1, [''])
+ def command_recolor(self, args):
"""
/recolor [random]
Re-assign color to the participants of the room
"""
- arg = arg.strip()
+ deterministic = config.get_by_tabname('deterministic_nick_colors', self.name)
+ if deterministic:
+ for user in self.users:
+ if user.nick == self.own_nick:
+ continue
+ color = self.search_for_color(user.nick)
+ if color != '':
+ continue
+ user.set_deterministic_color()
+ if args[0] == 'random':
+ self.core.information('"random" was provided, but poezio is '
+ 'configured to use deterministic colors',
+ 'Warning')
+ self.user_win.refresh(self.users)
+ self.input.refresh()
+ return
compare_users = lambda x: x.last_talked
users = list(self.users)
sorted_users = sorted(users, key=compare_users, reverse=True)
+ full_sorted_users = sorted_users[:]
# search our own user, to remove it from the list
- for user in sorted_users:
+ # Also remove users whose color is fixed
+ for user in full_sorted_users:
+ color = self.search_for_color(user.nick)
if user.nick == self.own_nick:
sorted_users.remove(user)
user.color = get_theme().COLOR_OWN_NICK
+ elif color != '':
+ sorted_users.remove(user)
+ user.change_color(color, deterministic)
colors = list(get_theme().LIST_COLOR_NICKNAMES)
- if arg and arg == 'random':
+ if args[0] == 'random':
random.shuffle(colors)
for i, user in enumerate(sorted_users):
user.color = colors[i % len(colors)]
@@ -420,41 +463,86 @@ class MucTab(ChatTab):
self.text_win.refresh()
self.input.refresh()
- def command_version(self, arg):
+ @command_args_parser.quoted(2, 2, [''])
+ def command_color(self, args):
+ """
+ /color <nick> <color>
+ Fix a color for a nick.
+ Use "unset" instead of a color to remove the attribution.
+ User "random" to attribute a random color.
+ """
+ if args is None:
+ return self.core.command_help('color')
+ nick = args[0]
+ color = args[1].lower()
+ user = self.get_user_by_name(nick)
+ if not color in xhtml.colors and color not in ('unset', 'random'):
+ return self.core.information("Unknown color: %s" % color, 'Error')
+ if user and user.nick == self.own_nick:
+ return self.core.information("You cannot change the color of your"
+ " own nick.", 'Error')
+ if color == 'unset':
+ if config.remove_and_save(nick, 'muc_colors'):
+ self.core.information('Color for nick %s unset' % (nick))
+ else:
+ if color == 'random':
+ color = random.choice(list(xhtml.colors))
+ if user:
+ user.change_color(color)
+ config.set_and_save(nick, color, 'muc_colors')
+ nick_color_aliases = config.get_by_tabname('nick_color_aliases', self.name)
+ if nick_color_aliases:
+ # if any user in the room has a nick which is an alias of the
+ # nick, update its color
+ for tab in self.core.get_tabs(MucTab):
+ for u in tab.users:
+ nick_alias = re.sub('^_*', '', u.nick)
+ nick_alias = re.sub('_*$', '', nick_alias)
+ if nick_alias == nick:
+ u.change_color(color)
+ self.text_win.rebuild_everything(self._text_buffer)
+ self.user_win.refresh(self.users)
+ self.text_win.refresh()
+ self.input.refresh()
+
+ @command_args_parser.quoted(1)
+ def command_version(self, args):
"""
/version <jid or nick>
"""
def callback(res):
if not res:
- return self.core.information(_('Could not get the software '
- 'version from %s') % (jid,),
- _('Warning'))
- version = _('%s is running %s version %s on %s') % (
+ return self.core.information('Could not get the software '
+ 'version from %s' % (jid,),
+ 'Warning')
+ version = '%s is running %s version %s on %s' % (
jid,
- res.get('name') or _('an unknown software'),
- res.get('version') or _('unknown'),
- res.get('os') or _('an unknown platform'))
+ res.get('name') or 'an unknown software',
+ res.get('version') or 'unknown',
+ res.get('os') or 'an unknown platform')
self.core.information(version, 'Info')
- if not arg:
+ if args is None:
return self.core.command_help('version')
- if arg in [user.nick for user in self.users]:
+ nick = args[0]
+ if nick in [user.nick for user in self.users]:
jid = safeJID(self.name).bare
- jid = safeJID(jid + '/' + arg)
+ jid = safeJID(jid + '/' + nick)
else:
- jid = safeJID(arg)
+ jid = safeJID(nick)
fixes.get_version(self.core.xmpp, jid,
- callback=callback)
+ callback=callback)
- def command_nick(self, arg):
+ @command_args_parser.quoted(1)
+ def command_nick(self, args):
"""
/nick <nickname>
"""
- if not arg:
+ if args is None:
return self.core.command_help('nick')
- nick = arg
+ nick = args[0]
if not self.joined:
- return self.core.information(_('/nick only works in joined rooms'),
- _('Info'))
+ return self.core.information('/nick only works in joined rooms',
+ 'Info')
current_status = self.core.get_status()
if not safeJID(self.name + '/' + nick):
return self.core.information('Invalid nick', 'Info')
@@ -462,11 +550,12 @@ class MucTab(ChatTab):
current_status.message,
current_status.show)
- def command_part(self, arg):
+ @command_args_parser.quoted(0, 1, [''])
+ def command_part(self, args):
"""
/part [msg]
"""
- arg = arg.strip()
+ arg = args[0]
msg = None
if self.joined:
info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
@@ -480,24 +569,24 @@ class MucTab(ChatTab):
color = 3
if arg:
- msg = _('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
- 'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
- ' left the chatroom'
- ' (\x19o%(reason)s\x19%(info_col)s})') % {
- 'info_col': info_col, 'reason': arg,
- 'spec': char_quit, 'color': color,
- 'color_spec': spec_col,
- 'nick': self.own_nick,
- }
+ msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
+ 'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
+ ' left the chatroom'
+ ' (\x19o%(reason)s\x19%(info_col)s})') % {
+ 'info_col': info_col, 'reason': arg,
+ 'spec': char_quit, 'color': color,
+ 'color_spec': spec_col,
+ 'nick': self.own_nick,
+ }
else:
- msg = _('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
- 'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
- ' left the chatroom') % {
- 'info_col': info_col,
- 'spec': char_quit, 'color': color,
- 'color_spec': spec_col,
- 'nick': self.own_nick,
- }
+ msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
+ 'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
+ ' left the chatroom') % {
+ 'info_col': info_col,
+ 'spec': char_quit, 'color': color,
+ 'color_spec': spec_col,
+ 'nick': self.own_nick,
+ }
self.add_message(msg, typ=2)
self.disconnect()
@@ -507,49 +596,52 @@ class MucTab(ChatTab):
self.refresh()
self.core.doupdate()
- def command_close(self, arg):
+ @command_args_parser.raw
+ def command_close(self, msg):
"""
/close [msg]
"""
- self.command_part(arg)
+ self.command_part(msg)
self.core.close_tab()
- def command_query(self, arg):
+ @command_args_parser.quoted(1, 1)
+ def command_query(self, args):
"""
/query <nick> [message]
"""
- args = common.shell_split(arg)
- if len(args) < 1:
- return
+ if args is None:
+ return self.core.command_help('query')
nick = args[0]
r = None
for user in self.users:
if user.nick == nick:
r = self.core.open_private_window(self.name, user.nick)
- if r and len(args) > 1:
+ if r and len(args) == 2:
msg = args[1]
self.core.current_tab().command_say(
xhtml.convert_simple_to_full_colors(msg))
if not r:
- self.core.information(_("Cannot find user: %s" % nick), 'Error')
+ self.core.information("Cannot find user: %s" % nick, 'Error')
- def command_topic(self, arg):
+ @command_args_parser.raw
+ def command_topic(self, subject):
"""
/topic [new topic]
"""
- if not arg.strip():
+ if not subject:
self._text_buffer.add_message(
- _("\x19%s}The subject of the room is: %s %s") %
+ "\x19%s}The subject of the room is: %s %s" %
(dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
self.topic,
'(set by %s)' % self.topic_from if self.topic_from
else ''))
self.refresh()
return
- subject = arg
+
muc.change_subject(self.core.xmpp, self.name, subject)
- def command_names(self, arg=None):
+ @command_args_parser.quoted(0)
+ def command_names(self, args):
"""
/names
"""
@@ -620,29 +712,28 @@ class MucTab(ChatTab):
return the_input.new_completion(word_list, 1, quotify=True)
- def command_kick(self, arg):
+ @command_args_parser.quoted(1, 1)
+ def command_kick(self, args):
"""
/kick <nick> [reason]
"""
- args = common.shell_split(arg)
- if not args:
- self.core.command_help('kick')
+ if args is None:
+ return self.core.command_help('kick')
+ if len(args) == 2:
+ msg = ' "%s"' % args[1]
else:
- if len(args) > 1:
- msg = ' "%s"' % args[1]
- else:
- msg = ''
- self.command_role('"'+args[0]+ '" none'+msg)
+ msg = ''
+ self.command_role('"'+args[0]+ '" none'+msg)
- def command_ban(self, arg):
+ @command_args_parser.quoted(1, 1)
+ def command_ban(self, args):
"""
/ban <nick> [reason]
"""
def callback(iq):
if iq['type'] == 'error':
self.core.room_error(iq, self.name)
- args = common.shell_split(arg)
- if not args:
+ if args is None:
return self.core.command_help('ban')
if len(args) > 1:
msg = args[1]
@@ -661,7 +752,8 @@ class MucTab(ChatTab):
if not res:
self.core.information('Could not ban user', 'Error')
- def command_role(self, arg):
+ @command_args_parser.quoted(2, 1, [''])
+ def command_role(self, args):
"""
/role <nick> <role> [reason]
Changes the role of an user
@@ -670,24 +762,25 @@ class MucTab(ChatTab):
def callback(iq):
if iq['type'] == 'error':
self.core.room_error(iq, self.name)
- args = common.shell_split(arg)
- if len(args) < 2:
- self.core.command_help('role')
- return
- nick, role = args[0], args[1]
- if len(args) > 2:
- reason = ' '.join(args[2:])
- else:
- reason = ''
- if not self.joined or \
- not role in ('none', 'visitor', 'participant', 'moderator'):
- return
+
+ if args is None:
+ return self.core.command_help('role')
+
+ nick, role, reason = args[0], args[1].lower(), args[2]
+
+ valid_roles = ('none', 'visitor', 'participant', 'moderator')
+
+ if not self.joined or role not in valid_roles:
+ return self.core.information('The role must be one of ' + ', '.join(valid_roles),
+ 'Error')
+
if not safeJID(self.name + '/' + nick):
- return self.core('Invalid nick', 'Info')
+ return self.core.information('Invalid nick', 'Info')
muc.set_user_role(self.core.xmpp, self.name, nick, reason, role,
callback=callback)
- def command_affiliation(self, arg):
+ @command_args_parser.quoted(2)
+ def command_affiliation(self, args):
"""
/affiliation <nick> <role>
Changes the affiliation of an user
@@ -696,16 +789,20 @@ class MucTab(ChatTab):
def callback(iq):
if iq['type'] == 'error':
self.core.room_error(iq, self.name)
- args = common.shell_split(arg)
- if len(args) < 2:
- self.core.command_help('affiliation')
- return
+
+ if args is None:
+ return self.core.command_help('affiliation')
+
nick, affiliation = args[0], args[1].lower()
+
if not self.joined:
return
- if affiliation not in ('outcast', 'none', 'member', 'admin', 'owner'):
- self.core.command_help('affiliation')
- return
+
+ valid_affiliations = ('outcast', 'none', 'member', 'admin', 'owner')
+ if affiliation not in valid_affiliations:
+ return self.core.information('The affiliation must be one of ' + ', '.join(valid_affiliations),
+ 'Error')
+
if nick in [user.nick for user in self.users]:
res = muc.set_user_affiliation(self.core.xmpp, self.name,
affiliation, nick=nick,
@@ -715,8 +812,9 @@ class MucTab(ChatTab):
affiliation, jid=safeJID(nick),
callback=callback)
if not res:
- self.core.information(_('Could not set affiliation'), _('Error'))
+ self.core.information('Could not set affiliation', 'Error')
+ @command_args_parser.raw
def command_say(self, line, correct=False):
"""
/say <message>
@@ -755,45 +853,48 @@ class MucTab(ChatTab):
msg.send()
self.chat_state = needed
- def command_xhtml(self, arg):
- message = self.generate_xhtml_message(arg)
+ @command_args_parser.raw
+ def command_xhtml(self, msg):
+ message = self.generate_xhtml_message(msg)
if message:
message['type'] = 'groupchat'
message.send()
- def command_ignore(self, arg):
+ @command_args_parser.quoted(1)
+ def command_ignore(self, args):
"""
/ignore <nick>
"""
- if not arg:
- self.core.command_help('ignore')
- return
- nick = arg
+ if args is None:
+ return self.core.command_help('ignore')
+
+ nick = args[0]
user = self.get_user_by_name(nick)
if not user:
- self.core.information(_('%s is not in the room') % nick)
+ self.core.information('%s is not in the room' % nick)
elif user in self.ignores:
- self.core.information(_('%s is already ignored') % nick)
+ self.core.information('%s is already ignored' % nick)
else:
self.ignores.append(user)
- self.core.information(_("%s is now ignored") % nick, 'info')
+ self.core.information("%s is now ignored" % nick, 'info')
- def command_unignore(self, arg):
+ @command_args_parser.quoted(1)
+ def command_unignore(self, args):
"""
/unignore <nick>
"""
- if not arg:
- self.core.command_help('unignore')
- return
- nick = arg
+ if args is None:
+ return self.core.command_help('unignore')
+
+ nick = args[0]
user = self.get_user_by_name(nick)
if not user:
- self.core.information(_('%s is not in the room') % nick)
+ self.core.information('%s is not in the room' % nick)
elif user not in self.ignores:
- self.core.information(_('%s is not ignored') % nick)
+ self.core.information('%s is not ignored' % nick)
else:
self.ignores.remove(user)
- self.core.information(_('%s is now unignored') % nick)
+ self.core.information('%s is now unignored' % nick)
def completion_unignore(self, the_input):
if the_input.get_argument_position() == 1:
@@ -980,12 +1081,14 @@ class MucTab(ChatTab):
role = presence['muc']['role']
jid = presence['muc']['jid']
typ = presence['type']
+ deterministic = config.get_by_tabname('deterministic_nick_colors', self.name)
+ color = self.search_for_color(from_nick)
if not self.joined: # user in the room BEFORE us.
# ignore redondant presence message, see bug #1509
if (from_nick not in [user.nick for user in self.users]
and typ != "unavailable"):
new_user = User(from_nick, affiliation, show,
- status, role, jid)
+ status, role, jid, deterministic, color)
self.users.append(new_user)
self.core.events.trigger('muc_join', presence, self)
if '110' in status_codes or self.own_nick == from_nick:
@@ -1015,9 +1118,9 @@ class MucTab(ChatTab):
spec_col = dump_tuple(get_theme().COLOR_JOIN_CHAR)
self.add_message(
- _('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} You '
- '(\x19%(nick_col)s}%(nick)s\x19%(info_col)s}) joined'
- ' the chatroom') %
+ '\x19%(color_spec)s}%(spec)s\x19%(info_col)s} You '
+ '(\x19%(nick_col)s}%(nick)s\x19%(info_col)s}) joined'
+ ' the chatroom' %
{
'nick': from_nick,
'spec': get_theme().CHAR_JOIN,
@@ -1028,21 +1131,21 @@ class MucTab(ChatTab):
typ=2)
if '201' in status_codes:
self.add_message(
- _('\x19%(info_col)s}Info: The room '
- 'has been created') %
+ '\x19%(info_col)s}Info: The room '
+ 'has been created' %
{'info_col': info_col},
typ=0)
if '170' in status_codes:
self.add_message(
- _('\x19%(warn_col)s}Warning:\x19%(info_col)s}'
- ' This room is publicly logged') %
+ '\x19%(warn_col)s}Warning:\x19%(info_col)s}'
+ ' This room is publicly logged' %
{'info_col': info_col,
'warn_col': warn_col},
typ=0)
if '100' in status_codes:
self.add_message(
- _('\x19%(warn_col)s}Warning:\x19%(info_col)s}'
- ' This room is not anonymous.') %
+ '\x19%(warn_col)s}Warning:\x19%(info_col)s}'
+ ' This room is not anonymous.' %
{'info_col': info_col,
'warn_col': warn_col},
typ=0)
@@ -1065,7 +1168,7 @@ class MucTab(ChatTab):
if not user:
self.core.events.trigger('muc_join', presence, self)
self.on_user_join(from_nick, affiliation, show, status, role,
- jid)
+ jid, color)
# nick change
elif change_nick:
self.core.events.trigger('muc_nickchange', presence, self)
@@ -1105,8 +1208,8 @@ class MucTab(ChatTab):
def on_non_member_kicked(self):
"""We have been kicked because the MUC is members-only"""
self.add_message(
- _('\x19%(info_col)s}You have been kicked because you '
- 'are not a member and the room is now members-only.') % {
+ '\x19%(info_col)s}You have been kicked because you '
+ 'are not a member and the room is now members-only.' % {
'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
typ=2)
self.disconnect()
@@ -1114,18 +1217,19 @@ class MucTab(ChatTab):
def on_muc_shutdown(self):
"""We have been kicked because the MUC service is shutting down"""
self.add_message(
- _('\x19%(info_col)s}You have been kicked because the'
- ' MUC service is shutting down.') % {
+ '\x19%(info_col)s}You have been kicked because the'
+ ' MUC service is shutting down.' % {
'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
typ=2)
self.disconnect()
- def on_user_join(self, from_nick, affiliation, show, status, role, jid):
+ def on_user_join(self, from_nick, affiliation, show, status, role, jid, color):
"""
When a new user joins the groupchat
"""
+ deterministic = config.get_by_tabname('deterministic_nick_colors', self.name)
user = User(from_nick, affiliation,
- show, status, role, jid)
+ show, status, role, jid, deterministic, color)
self.users.append(user)
hide_exit_join = config.get_by_tabname('hide_exit_join',
self.general_jid)
@@ -1139,17 +1243,17 @@ class MucTab(ChatTab):
spec_col = dump_tuple(get_theme().COLOR_JOIN_CHAR)
char_join = get_theme().CHAR_JOIN
if not jid.full:
- msg = _('\x19%(color_spec)s}%(spec)s \x19%(color)s}%(nick)s'
- '\x19%(info_col)s} joined the chatroom') % {
+ msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}%(nick)s'
+ '\x19%(info_col)s} joined the chatroom') % {
'nick': from_nick, 'spec': char_join,
'color': color,
'info_col': info_col,
'color_spec': spec_col,
}
else:
- msg = _('\x19%(color_spec)s}%(spec)s \x19%(color)s}%(nick)s '
- '\x19%(info_col)s}(\x19%(jid_color)s}%(jid)s\x19'
- '%(info_col)s}) joined the chatroom') % {
+ msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}%(nick)s '
+ '\x19%(info_col)s}(\x19%(jid_color)s}%(jid)s\x19'
+ '%(info_col)s}) joined the chatroom') % {
'spec': char_join, 'nick': from_nick,
'color':color, 'jid':jid.full,
'info_col': info_col,
@@ -1166,6 +1270,12 @@ class MucTab(ChatTab):
self.own_nick = new_nick
# also change our nick in all private discussions of this room
self.core.on_muc_own_nickchange(self)
+ else:
+ color = config.get_by_tabname(new_nick, 'muc_colors')
+ if color != '':
+ deterministic = config.get_by_tabname('deterministic_nick_colors',
+ self.name)
+ user.change_color(color, deterministic)
user.change_nick(new_nick)
if config.get_by_tabname('display_user_color_in_join_part',
@@ -1174,8 +1284,8 @@ class MucTab(ChatTab):
else:
color = 3
info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
- self.add_message(_('\x19%(color)s}%(old)s\x19%(info_col)s} is'
- ' now known as \x19%(color)s}%(new)s') % {
+ self.add_message('\x19%(color)s}%(old)s\x19%(info_col)s} is'
+ ' now known as \x19%(color)s}%(new)s' % {
'old':from_nick, 'new':new_nick,
'color':color, 'info_col': info_col},
typ=2)
@@ -1198,13 +1308,13 @@ class MucTab(ChatTab):
if from_nick == self.own_nick: # we are banned
if by:
- kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s}'
- ' have been banned by \x194}%(by)s') % {
+ kick_msg = ('\x191}%(spec)s \x193}You\x19%(info_col)s}'
+ ' have been banned by \x194}%(by)s') % {
'spec': char_kick, 'by': by,
'info_col': info_col}
else:
- kick_msg = _('\x191}%(spec)s \x193}You\x19'
- '%(info_col)s} have been banned.') % {
+ kick_msg = ('\x191}%(spec)s \x193}You\x19'
+ '%(info_col)s} have been banned.') % {
'spec': char_kick, 'info_col': info_col}
self.core.disable_private_tabs(self.name, reason=kick_msg)
self.disconnect()
@@ -1233,20 +1343,20 @@ class MucTab(ChatTab):
color = 3
if by:
- kick_msg = _('\x191}%(spec)s \x19%(color)s}'
- '%(nick)s\x19%(info_col)s} '
- 'has been banned by \x194}%(by)s') % {
+ kick_msg = ('\x191}%(spec)s \x19%(color)s}'
+ '%(nick)s\x19%(info_col)s} '
+ 'has been banned by \x194}%(by)s') % {
'spec': char_kick, 'nick': from_nick,
'color': color, 'by': by,
'info_col': info_col}
else:
- kick_msg = _('\x191}%(spec)s \x19%(color)s}%(nick)s'
- '\x19%(info_col)s} has been banned') % {
+ kick_msg = ('\x191}%(spec)s \x19%(color)s}%(nick)s'
+ '\x19%(info_col)s} has been banned') % {
'spec': char_kick, 'nick': from_nick,
'color': color, 'info_col': info_col}
if reason is not None and reason.text:
- kick_msg += _('\x19%(info_col)s} Reason: \x196}'
- '%(reason)s\x19%(info_col)s}') % {
+ kick_msg += ('\x19%(info_col)s} Reason: \x196}'
+ '%(reason)s\x19%(info_col)s}') % {
'reason': reason.text, 'info_col': info_col}
self.add_message(kick_msg, typ=2)
@@ -1266,14 +1376,14 @@ class MucTab(ChatTab):
by = actor_elem.get('nick') or actor_elem.get('jid')
if from_nick == self.own_nick: # we are kicked
if by:
- kick_msg = _('\x191}%(spec)s \x193}You\x19'
- '%(info_col)s} have been kicked'
- ' by \x193}%(by)s') % {
+ kick_msg = ('\x191}%(spec)s \x193}You\x19'
+ '%(info_col)s} have been kicked'
+ ' by \x193}%(by)s') % {
'spec': char_kick, 'by': by,
'info_col': info_col}
else:
- kick_msg = _('\x191}%(spec)s \x193}You\x19%(info_col)s}'
- ' have been kicked.') % {
+ kick_msg = ('\x191}%(spec)s \x193}You\x19%(info_col)s}'
+ ' have been kicked.') % {
'spec': char_kick,
'info_col': info_col}
self.core.disable_private_tabs(self.name, reason=kick_msg)
@@ -1302,19 +1412,19 @@ class MucTab(ChatTab):
else:
color = 3
if by:
- kick_msg = _('\x191}%(spec)s \x19%(color)s}%(nick)s'
- '\x19%(info_col)s} has been kicked by '
- '\x193}%(by)s') % {
+ kick_msg = ('\x191}%(spec)s \x19%(color)s}%(nick)s'
+ '\x19%(info_col)s} has been kicked by '
+ '\x193}%(by)s') % {
'spec': char_kick, 'nick':from_nick,
'color':color, 'by':by, 'info_col': info_col}
else:
- kick_msg = _('\x191}%(spec)s \x19%(color)s}%(nick)s'
- '\x19%(info_col)s} has been kicked') % {
+ kick_msg = ('\x191}%(spec)s \x19%(color)s}%(nick)s'
+ '\x19%(info_col)s} has been kicked') % {
'spec': char_kick, 'nick': from_nick,
'color':color, 'info_col': info_col}
if reason is not None and reason.text:
- kick_msg += _('\x19%(info_col)s} Reason: \x196}'
- '%(reason)s') % {
+ kick_msg += ('\x19%(info_col)s} Reason: \x196}'
+ '%(reason)s') % {
'reason': reason.text, 'info_col': info_col}
self.add_message(kick_msg, typ=2)
@@ -1343,19 +1453,19 @@ class MucTab(ChatTab):
spec_col = dump_tuple(get_theme().COLOR_QUIT_CHAR)
if not jid.full:
- leave_msg = _('\x19%(color_spec)s}%(spec)s \x19%(color)s}'
- '%(nick)s\x19%(info_col)s} has left the '
- 'chatroom') % {
+ leave_msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}'
+ '%(nick)s\x19%(info_col)s} has left the '
+ 'chatroom') % {
'nick':from_nick, 'color':color,
'spec':get_theme().CHAR_QUIT,
'info_col': info_col,
'color_spec': spec_col}
else:
jid_col = dump_tuple(get_theme().COLOR_MUC_JID)
- leave_msg = _('\x19%(color_spec)s}%(spec)s \x19%(color)s}'
- '%(nick)s\x19%(info_col)s} (\x19%(jid_col)s}'
- '%(jid)s\x19%(info_col)s}) has left the '
- 'chatroom') % {
+ leave_msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}'
+ '%(nick)s\x19%(info_col)s} (\x19%(jid_col)s}'
+ '%(jid)s\x19%(info_col)s}) has left the '
+ 'chatroom') % {
'spec':get_theme().CHAR_QUIT,
'nick':from_nick, 'color':color,
'jid':jid.full, 'info_col': info_col,
@@ -1381,33 +1491,29 @@ class MucTab(ChatTab):
else:
color = 3
if from_nick == self.own_nick:
- msg = _('\x19%(color)s}You\x19%(info_col)s} changed: ') % {
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
- 'color': color}
+ msg = '\x19%(color)s}You\x19%(info_col)s} changed: ' % {
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
+ 'color': color}
else:
- msg = _('\x19%(color)s}%(nick)s\x19%(info_col)s} changed: ') % {
- 'nick': from_nick, 'color': color,
- 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
- if show not in SHOW_NAME:
- self.core.information(_("%s from room %s sent an invalid show: %s")
- % (from_nick, from_room, show),
- _("Warning"))
+ msg = '\x19%(color)s}%(nick)s\x19%(info_col)s} changed: ' % {
+ 'nick': from_nick, 'color': color,
+ 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}
if affiliation != user.affiliation:
- msg += _('affiliation: %s, ') % affiliation
+ msg += 'affiliation: %s, ' % affiliation
display_message = True
if role != user.role:
- msg += _('role: %s, ') % role
+ msg += 'role: %s, ' % role
display_message = True
if show != user.show and show in SHOW_NAME:
- msg += _('show: %s, ') % SHOW_NAME[show]
+ msg += 'show: %s, ' % SHOW_NAME[show]
display_message = True
if status != user.status:
# if the user sets his status to nothing
if status:
- msg += _('status: %s, ') % status
+ msg += 'status: %s, ' % status
display_message = True
elif show in SHOW_NAME and show == user.show:
- msg += _('show: %s, ') % SHOW_NAME[show]
+ msg += 'show: %s, ' % SHOW_NAME[show]
display_message = True
if not display_message:
return
@@ -1461,8 +1567,8 @@ class MucTab(ChatTab):
"""
if time is None and self.joined: # don't log the history messages
if not logger.log_message(self.name, nickname, txt, typ=typ):
- self.core.information(_('Unable to write in the log file'),
- _('Error'))
+ self.core.information('Unable to write in the log file',
+ 'Error')
def do_highlight(self, txt, time, nickname):
"""
@@ -1581,6 +1687,22 @@ class MucTab(ChatTab):
else: # Re-send a self-ping in a few seconds
self.enable_self_ping_event()
+ def search_for_color(self, nick):
+ """
+ Search for the color of a nick in the config file.
+ Also, look at the colors of its possible aliases if nick_color_aliases
+ is set.
+ """
+ color = config.get_by_tabname(nick, 'muc_colors')
+ if color != '':
+ return color
+ nick_color_aliases = config.get_by_tabname('nick_color_aliases', self.name)
+ if nick_color_aliases:
+ nick_alias = re.sub('^_*', '', nick)
+ nick_alias = re.sub('_*$', '', nick_alias)
+ color = config.get_by_tabname(nick_alias, 'muc_colors')
+ return color
+
def on_self_ping_failed(self, iq):
self.command_part("the MUC server is not responding")
self.core.refresh_window()
diff --git a/src/tabs/privatetab.py b/src/tabs/privatetab.py
index 4c01cd70..a715a922 100644
--- a/src/tabs/privatetab.py
+++ b/src/tabs/privatetab.py
@@ -10,8 +10,6 @@ both participant’s nicks. It also has slightly different features than
the ConversationTab (such as tab-completion on nicks from the room).
"""
-from gettext import gettext as _
-
import logging
log = logging.getLogger(__name__)
@@ -27,6 +25,7 @@ from config import config
from decorators import refresh_wrapper
from logger import logger
from theming import get_theme, dump_tuple
+from decorators import command_args_parser
class PrivateTab(OneToOneTab):
"""
@@ -48,15 +47,15 @@ class PrivateTab(OneToOneTab):
self.key_func['^I'] = self.completion
# commands
self.register_command('info', self.command_info,
- desc=_('Display some information about the user in the MUC: its/his/her role, affiliation, status and status message.'),
- shortdesc=_('Info about the user.'))
+ desc='Display some information about the user in the MUC: its/his/her role, affiliation, status and status message.',
+ shortdesc='Info about the user.')
self.register_command('unquery', self.command_unquery,
- shortdesc=_('Close the tab.'))
+ shortdesc='Close the tab.')
self.register_command('close', self.command_unquery,
- shortdesc=_('Close the tab.'))
+ shortdesc='Close the tab.')
self.register_command('version', self.command_version,
- desc=_('Get the software version of the current interlocutor (usually its XMPP client and Operating System).'),
- shortdesc=_('Get the software version of a jid.'))
+ desc='Get the software version of the current interlocutor (usually its XMPP client and Operating System).',
+ shortdesc='Get the software version of a jid.')
self.resize()
self.parent_muc = self.core.get_tab_by_name(safeJID(name).bare, MucTab)
self.on = True
@@ -87,13 +86,14 @@ class PrivateTab(OneToOneTab):
def load_logs(self, log_nb):
logs = logger.get_logs(safeJID(self.name).full.replace('/', '\\'), log_nb)
+ return logs
def log_message(self, txt, nickname, time=None, typ=1):
"""
Log the messages in the archives.
"""
if not logger.log_message(self.name, nickname, txt, date=time, typ=typ):
- self.core.information(_('Unable to write in the log file'), 'Error')
+ self.core.information('Unable to write in the log file', 'Error')
def on_close(self):
self.parent_muc.privates.remove(self)
@@ -120,6 +120,7 @@ class PrivateTab(OneToOneTab):
empty_after = self.input.get_text() == '' or (self.input.get_text().startswith('/') and not self.input.get_text().startswith('//'))
self.send_composing_chat_state(empty_after)
+ @command_args_parser.raw
def command_say(self, line, attention=False, correct=False):
if not self.on:
return
@@ -182,13 +183,15 @@ class PrivateTab(OneToOneTab):
self.text_win.refresh()
self.input.refresh()
- def command_unquery(self, arg):
+ @command_args_parser.ignored
+ def command_unquery(self):
"""
/unquery
"""
self.core.close_tab()
- def command_version(self, arg):
+ @command_args_parser.quoted(0, 1)
+ def command_version(self, args):
"""
/version
"""
@@ -196,22 +199,23 @@ class PrivateTab(OneToOneTab):
if not res:
return self.core.information('Could not get the software version from %s' % (jid,), 'Warning')
version = '%s is running %s version %s on %s' % (jid,
- res.get('name') or _('an unknown software'),
- res.get('version') or _('unknown'),
- res.get('os') or _('an unknown platform'))
+ res.get('name') or 'an unknown software',
+ res.get('version') or 'unknown',
+ res.get('os') or 'an unknown platform')
self.core.information(version, 'Info')
- if arg:
- return self.core.command_version(arg)
+ if args:
+ return self.core.command_version(args[0])
jid = safeJID(self.name)
fixes.get_version(self.core.xmpp, jid,
callback=callback)
+ @command_args_parser.quoted(0, 1)
def command_info(self, arg):
"""
/info
"""
- if arg:
- self.parent_muc.command_info(arg)
+ if arg and arg[0]:
+ self.parent_muc.command_info(arg[0])
else:
user = safeJID(self.name).resource
self.parent_muc.command_info(user)
@@ -319,9 +323,9 @@ class PrivateTab(OneToOneTab):
"""
self.deactivate()
if not status_message:
- self.add_message(_('\x191}%(spec)s \x193}%(nick)s\x19%(info_col)s} has left the room') % {'nick':from_nick, 'spec':get_theme().CHAR_QUIT, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2)
+ self.add_message('\x191}%(spec)s \x193}%(nick)s\x19%(info_col)s} has left the room' % {'nick':from_nick, 'spec':get_theme().CHAR_QUIT, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2)
else:
- self.add_message(_('\x191}%(spec)s \x193}%(nick)s\x19%(info_col)s} has left the room (%(status)s)"') % {'nick':from_nick, 'spec':get_theme().CHAR_QUIT, 'status': status_message, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2)
+ self.add_message('\x191}%(spec)s \x193}%(nick)s\x19%(info_col)s} has left the room (%(status)s)"' % {'nick':from_nick, 'spec':get_theme().CHAR_QUIT, 'status': status_message, 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}, typ=2)
return self.core.current_tab() is self
@refresh_wrapper.conditional
diff --git a/src/tabs/rostertab.py b/src/tabs/rostertab.py
index 878e89ed..aaff7de3 100644
--- a/src/tabs/rostertab.py
+++ b/src/tabs/rostertab.py
@@ -5,15 +5,16 @@ rectangle shows the current contact info.
This module also includes functions to match users in the roster.
"""
-from gettext import gettext as _
-
import logging
log = logging.getLogger(__name__)
+import base64
import curses
import difflib
import os
+import ssl
from os import getenv, path
+from functools import partial
from . import Tab
@@ -25,6 +26,7 @@ from contact import Contact, Resource
from decorators import refresh_wrapper
from roster import RosterGroup, roster
from theming import get_theme, dump_tuple
+from decorators import command_args_parser
class RosterInfoTab(Tab):
"""
@@ -44,107 +46,315 @@ class RosterInfoTab(Tab):
self.input = self.default_help_message
self.state = 'normal'
self.key_func['^I'] = self.completion
- self.key_func[' '] = self.on_space
self.key_func["/"] = self.on_slash
- self.key_func["KEY_UP"] = self.move_cursor_up
- self.key_func["KEY_DOWN"] = self.move_cursor_down
- self.key_func["M-u"] = self.move_cursor_to_next_contact
- self.key_func["M-y"] = self.move_cursor_to_prev_contact
- self.key_func["M-U"] = self.move_cursor_to_next_group
- self.key_func["M-Y"] = self.move_cursor_to_prev_group
- self.key_func["M-[1;5B"] = self.move_cursor_to_next_group
- self.key_func["M-[1;5A"] = self.move_cursor_to_prev_group
- self.key_func["l"] = self.command_last_activity
- self.key_func["o"] = self.toggle_offline_show
- self.key_func["v"] = self.get_contact_version
- self.key_func["i"] = self.show_contact_info
- self.key_func["n"] = self.change_contact_name
- self.key_func["s"] = self.start_search
- self.key_func["S"] = self.start_search_slow
- self.register_command('deny', self.command_deny,
- usage=_('[jid]'),
- desc=_('Deny your presence to the provided JID (or the selected contact in your roster), who is asking you to be in his/here roster.'),
- shortdesc=_('Deny an user your presence.'),
- completion=self.completion_deny)
- self.register_command('accept', self.command_accept,
- usage=_('[jid]'),
- desc=_('Allow the provided JID (or the selected contact in your roster), to see your presence.'),
- shortdesc=_('Allow an user your presence.'),
- completion=self.completion_deny)
- self.register_command('add', self.command_add,
- usage=_('<jid>'),
- desc=_('Add the specified JID to your roster, ask him to allow you to see his presence, and allow him to see your presence.'),
- shortdesc=_('Add an user to your roster.'))
- self.register_command('name', self.command_name,
- usage=_('<jid> <name>'),
- shortdesc=_('Set the given JID\'s name.'),
- completion=self.completion_name)
- self.register_command('groupadd', self.command_groupadd,
- usage=_('<jid> <group>'),
- desc=_('Add the given JID to the given group.'),
- shortdesc=_('Add an user to a group'),
- completion=self.completion_groupadd)
- self.register_command('groupmove', self.command_groupmove,
- usage=_('<jid> <old group> <new group>'),
- desc=_('Move the given JID from the old group to the new group.'),
- shortdesc=_('Move an user to another group.'),
- completion=self.completion_groupmove)
- self.register_command('groupremove', self.command_groupremove,
- usage=_('<jid> <group>'),
- desc=_('Remove the given JID from the given group.'),
- shortdesc=_('Remove an user from a group.'),
- completion=self.completion_groupremove)
- self.register_command('remove', self.command_remove,
- usage=_('[jid]'),
- desc=_('Remove the specified JID from your roster. This wil unsubscribe you from its presence, cancel its subscription to yours, and remove the item from your roster.'),
- shortdesc=_('Remove an user from your roster.'),
- completion=self.completion_remove)
+ # disable most of the roster features when in anonymous mode
+ if not self.core.xmpp.anon:
+ self.key_func[' '] = self.on_space
+ self.key_func["KEY_UP"] = self.move_cursor_up
+ self.key_func["KEY_DOWN"] = self.move_cursor_down
+ self.key_func["M-u"] = self.move_cursor_to_next_contact
+ self.key_func["M-y"] = self.move_cursor_to_prev_contact
+ self.key_func["M-U"] = self.move_cursor_to_next_group
+ self.key_func["M-Y"] = self.move_cursor_to_prev_group
+ self.key_func["M-[1;5B"] = self.move_cursor_to_next_group
+ self.key_func["M-[1;5A"] = self.move_cursor_to_prev_group
+ self.key_func["l"] = self.command_last_activity
+ self.key_func["o"] = self.toggle_offline_show
+ self.key_func["v"] = self.get_contact_version
+ self.key_func["i"] = self.show_contact_info
+ self.key_func["s"] = self.start_search
+ self.key_func["S"] = self.start_search_slow
+ self.key_func["n"] = self.change_contact_name
+ self.register_command('deny', self.command_deny,
+ usage='[jid]',
+ desc='Deny your presence to the provided JID (or the '
+ 'selected contact in your roster), who is asking'
+ 'you to be in his/here roster.',
+ shortdesc='Deny an user your presence.',
+ completion=self.completion_deny)
+ self.register_command('accept', self.command_accept,
+ usage='[jid]',
+ desc='Allow the provided JID (or the selected contact '
+ 'in your roster), to see your presence.',
+ shortdesc='Allow an user your presence.',
+ completion=self.completion_deny)
+ self.register_command('add', self.command_add,
+ usage='<jid>',
+ desc='Add the specified JID to your roster, ask him to'
+ ' allow you to see his presence, and allow him to'
+ ' see your presence.',
+ shortdesc='Add an user to your roster.')
+ self.register_command('name', self.command_name,
+ usage='<jid> [name]',
+ shortdesc='Set the given JID\'s name.',
+ completion=self.completion_name)
+ self.register_command('groupadd', self.command_groupadd,
+ usage='<jid> <group>',
+ desc='Add the given JID to the given group.',
+ shortdesc='Add an user to a group',
+ completion=self.completion_groupadd)
+ self.register_command('groupmove', self.command_groupmove,
+ usage='<jid> <old group> <new group>',
+ desc='Move the given JID from the old group to the new group.',
+ shortdesc='Move an user to another group.',
+ completion=self.completion_groupmove)
+ self.register_command('groupremove', self.command_groupremove,
+ usage='<jid> <group>',
+ desc='Remove the given JID from the given group.',
+ shortdesc='Remove an user from a group.',
+ completion=self.completion_groupremove)
+ self.register_command('remove', self.command_remove,
+ usage='[jid]',
+ desc='Remove the specified JID from your roster. This '
+ 'will unsubscribe you from its presence, cancel '
+ 'its subscription to yours, and remove the item '
+ 'from your roster.',
+ shortdesc='Remove an user from your roster.',
+ completion=self.completion_remove)
+ self.register_command('export', self.command_export,
+ usage='[/path/to/file]',
+ desc='Export your contacts into /path/to/file if '
+ 'specified, or $HOME/poezio_contacts if not.',
+ shortdesc='Export your roster to a file.',
+ completion=partial(self.completion_file, 1))
+ self.register_command('import', self.command_import,
+ usage='[/path/to/file]',
+ desc='Import your contacts from /path/to/file if '
+ 'specified, or $HOME/poezio_contacts if not.',
+ shortdesc='Import your roster from a file.',
+ completion=partial(self.completion_file, 1))
+ self.register_command('password', self.command_password,
+ usage='<password>',
+ shortdesc='Change your password')
+
self.register_command('reconnect', self.command_reconnect,
- desc=_('Disconnect from the remote server if you are currently connected and then connect to it again.'),
- shortdesc=_('Disconnect and reconnect to the server.'))
+ desc='Disconnect from the remote server if you are '
+ 'currently connected and then connect to it again.',
+ shortdesc='Disconnect and reconnect to the server.')
self.register_command('disconnect', self.command_disconnect,
- desc=_('Disconnect from the remote server.'),
- shortdesc=_('Disconnect from the server.'))
- self.register_command('export', self.command_export,
- usage=_('[/path/to/file]'),
- desc=_('Export your contacts into /path/to/file if specified, or $HOME/poezio_contacts if not.'),
- shortdesc=_('Export your roster to a file.'),
- completion=self.completion_file)
- self.register_command('import', self.command_import,
- usage=_('[/path/to/file]'),
- desc=_('Import your contacts from /path/to/file if specified, or $HOME/poezio_contacts if not.'),
- shortdesc=_('Import your roster from a file.'),
- completion=self.completion_file)
+ desc='Disconnect from the remote server.',
+ shortdesc='Disconnect from the server.')
self.register_command('clear', self.command_clear,
- shortdesc=_('Clear the info buffer.'))
+ shortdesc='Clear the info buffer.')
self.register_command('last_activity', self.command_last_activity,
- usage=_('<jid>'),
- desc=_('Informs you of the last activity of a JID.'),
- shortdesc=_('Get the activity of someone.'),
+ usage='<jid>',
+ desc='Informs you of the last activity of a JID.',
+ shortdesc='Get the activity of someone.',
completion=self.core.completion_last_activity)
- self.register_command('password', self.command_password,
- usage='<password>',
- shortdesc=_('Change your password'))
self.resize()
self.update_commands()
self.update_keys()
def check_blocking(self, features):
- if 'urn:xmpp:blocking' in features:
+ if 'urn:xmpp:blocking' in features and not self.core.xmpp.anon:
self.register_command('block', self.command_block,
- usage=_('[jid]'),
- shortdesc=_('Prevent a JID from talking to you.'),
+ usage='[jid]',
+ shortdesc='Prevent a JID from talking to you.',
completion=self.completion_block)
self.register_command('unblock', self.command_unblock,
- usage=_('[jid]'),
- shortdesc=_('Allow a JID to talk to you.'),
+ usage='[jid]',
+ shortdesc='Allow a JID to talk to you.',
completion=self.completion_unblock)
self.register_command('list_blocks', self.command_list_blocks,
- shortdesc=_('Show the blocked contacts.'))
+ shortdesc='Show the blocked contacts.')
self.core.xmpp.del_event_handler('session_start', self.check_blocking)
self.core.xmpp.add_event_handler('blocked_message', self.on_blocked_message)
+ def check_saslexternal(self, features):
+ if 'urn:xmpp:saslcert:1' in features and not self.core.xmpp.anon:
+ self.register_command('certs', self.command_certs,
+ desc='List the fingerprints of certificates'
+ ' which can connect to your account.',
+ shortdesc='List allowed client certs.')
+ self.register_command('cert_add', self.command_cert_add,
+ desc='Add a client certificate to the authorized ones. '
+ 'It must have an unique name and be contained in '
+ 'a PEM file. [management] is a boolean indicating'
+ ' if a client connected using this certificate can'
+ ' manage the certificates itself.',
+ shortdesc='Add a client certificate.',
+ usage='<name> <certificate path> [management]',
+ completion=self.completion_cert_add)
+ self.register_command('cert_disable', self.command_cert_disable,
+ desc='Remove a certificate from the list '
+ 'of allowed ones. Clients currently '
+ 'using this certificate will not be '
+ 'forcefully disconnected.',
+ shortdesc='Disable a certificate',
+ usage='<name>')
+ self.register_command('cert_revoke', self.command_cert_revoke,
+ desc='Remove a certificate from the list '
+ 'of allowed ones. Clients currently '
+ 'using this certificate will be '
+ 'forcefully disconnected.',
+ shortdesc='Revoke a certificate',
+ usage='<name>')
+ self.register_command('cert_fetch', self.command_cert_fetch,
+ desc='Retrieve a certificate with its '
+ 'name. It will be stored in <path>.',
+ shortdesc='Fetch a certificate',
+ usage='<name> <path>',
+ completion=self.completion_cert_fetch)
+
+ @command_args_parser.ignored
+ def command_certs(self):
+ """
+ /certs
+ """
+ def cb(iq):
+ if iq['type'] == 'error':
+ self.core.information('Unable to retrieve the certificate list.',
+ 'Error')
+ return
+ certs = []
+ for item in iq['sasl_certs']['items']:
+ users = '\n'.join(item['users'])
+ certs.append((item['name'], users))
+
+ if not certs:
+ return self.core.information('No certificates found', 'Info')
+ msg = 'Certificates:\n'
+ msg += '\n'.join(((' %s%s' % (item[0] + (': ' if item[1] else ''), item[1])) for item in certs))
+ self.core.information(msg, 'Info')
+
+ self.core.xmpp.plugin['xep_0257'].get_certs(callback=cb, timeout=3)
+
+ @command_args_parser.quoted(2, 1)
+ def command_cert_add(self, args):
+ """
+ /cert_add <name> <certfile> [cert-management]
+ """
+ if not args or len(args) < 2:
+ return self.core.command_help('cert_add')
+ def cb(iq):
+ if iq['type'] == 'error':
+ self.core.information('Unable to add the certificate.', 'Error')
+ else:
+ self.core.information('Certificate added.', 'Info')
+
+ name = args[0]
+
+ try:
+ with open(args[1]) as fd:
+ crt = fd.read()
+ crt = crt.replace(ssl.PEM_FOOTER, '').replace(ssl.PEM_HEADER, '').replace(' ', '').replace('\n', '')
+ except Exception as e:
+ self.core.information('Unable to read the certificate: %s' % e, 'Error')
+ return
+
+ if len(args) > 2:
+ management = args[2]
+ if management:
+ management = management.lower()
+ if management not in ('false', '0'):
+ management = True
+ else:
+ management = False
+ else:
+ management = False
+ else:
+ management = True
+
+ self.core.xmpp.plugin['xep_0257'].add_cert(name, crt, callback=cb,
+ allow_management=management)
+
+ def completion_cert_add(self, the_input):
+ """
+ completion for /cert_add <name> <path> [management]
+ """
+ text = the_input.get_text()
+ args = common.shell_split(text)
+ n = the_input.get_argument_position()
+ log.debug('%s %s %s', the_input.text, n, the_input.pos)
+ if n == 1:
+ return
+ elif n == 2:
+ return self.completion_file(2, the_input)
+ elif n == 3:
+ return the_input.new_completion(['true', 'false'], n)
+
+ @command_args_parser.quoted(1)
+ def command_cert_disable(self, args):
+ """
+ /cert_disable <name>
+ """
+ if not args:
+ return self.core.command_help('cert_disable')
+ def cb(iq):
+ if iq['type'] == 'error':
+ self.core.information('Unable to disable the certificate.', 'Error')
+ else:
+ self.core.information('Certificate disabled.', 'Info')
+
+ name = args[0]
+
+ self.core.xmpp.plugin['xep_0257'].disable_cert(name, callback=cb)
+
+ @command_args_parser.quoted(1)
+ def command_cert_revoke(self, args):
+ """
+ /cert_revoke <name>
+ """
+ if not args:
+ return self.core.command_help('cert_revoke')
+ def cb(iq):
+ if iq['type'] == 'error':
+ self.core.information('Unable to revoke the certificate.', 'Error')
+ else:
+ self.core.information('Certificate revoked.', 'Info')
+
+ name = args[0]
+
+ self.core.xmpp.plugin['xep_0257'].revoke_cert(name, callback=cb)
+
+
+ @command_args_parser.quoted(2)
+ def command_cert_fetch(self, args):
+ """
+ /cert_fetch <name> <path>
+ """
+ if not args or len(args) < 2:
+ return self.core.command_help('cert_fetch')
+ def cb(iq):
+ if iq['type'] == 'error':
+ self.core.information('Unable to fetch the certificate.',
+ 'Error')
+ return
+
+ cert = None
+ for item in iq['sasl_certs']['items']:
+ if item['name'] == name:
+ cert = base64.b64decode(item['x509cert'])
+ break
+
+ if not cert:
+ return self.core.information('Certificate not found.', 'Info')
+
+ cert = ssl.DER_cert_to_PEM_cert(cert)
+ with open(path, 'w') as fd:
+ fd.write(cert)
+
+ self.core.information('File stored at %s' % path, 'Info')
+
+ name = args[0]
+ path = args[1]
+
+ self.core.xmpp.plugin['xep_0257'].get_certs(callback=cb)
+
+ def completion_cert_fetch(self, the_input):
+ """
+ completion for /cert_fetch <name> <path>
+ """
+ text = the_input.get_text()
+ args = common.shell_split(text)
+ n = the_input.get_argument_position()
+ log.debug('%s %s %s', the_input.text, n, the_input.pos)
+ if n == 1:
+ return
+ elif n == 2:
+ return self.completion_file(2, the_input)
+
def on_blocked_message(self, message):
"""
When we try to send a message to a blocked contact
@@ -158,7 +368,8 @@ class RosterInfoTab(Tab):
}
tab.add_message(message)
- def command_block(self, arg):
+ @command_args_parser.quoted(0, 1)
+ def command_block(self, args):
"""
/block [jid]
"""
@@ -169,8 +380,8 @@ class RosterInfoTab(Tab):
return self.core.information('Contact blocked.', 'Info')
item = self.roster_win.selected_row
- if arg:
- jid = safeJID(arg)
+ if args:
+ jid = safeJID(args[0])
elif isinstance(item, Contact):
jid = item.bare_jid
elif isinstance(item, Resource):
@@ -185,7 +396,8 @@ class RosterInfoTab(Tab):
jids = roster.jids()
return the_input.new_completion(jids, 1, '', quotify=False)
- def command_unblock(self, arg):
+ @command_args_parser.quoted(0, 1)
+ def command_unblock(self, args):
"""
/unblock [jid]
"""
@@ -196,8 +408,8 @@ class RosterInfoTab(Tab):
return self.core.information('Contact unblocked.', 'Info')
item = self.roster_win.selected_row
- if arg:
- jid = safeJID(arg)
+ if args:
+ jid = safeJID(args[0])
elif isinstance(item, Contact):
jid = item.bare_jid
elif isinstance(item, Resource):
@@ -218,7 +430,8 @@ class RosterInfoTab(Tab):
self.core.xmpp.plugin['xep_0191'].get_blocked(callback=on_result)
return True
- def command_list_blocks(self, arg=None):
+ @command_args_parser.ignored
+ def command_list_blocks(self):
"""
/list_blocks
"""
@@ -236,7 +449,8 @@ class RosterInfoTab(Tab):
self.core.xmpp.plugin['xep_0191'].get_blocked(callback=callback)
- def command_reconnect(self, args=None):
+ @command_args_parser.ignored
+ def command_reconnect(self):
"""
/reconnect
"""
@@ -245,19 +459,21 @@ class RosterInfoTab(Tab):
else:
self.core.xmpp.connect()
- def command_disconnect(self, args=None):
+ @command_args_parser.ignored
+ def command_disconnect(self):
"""
/disconnect
"""
self.core.disconnect()
- def command_last_activity(self, arg=None):
+ @command_args_parser.quoted(0, 1)
+ def command_last_activity(self, args):
"""
/activity [jid]
"""
item = self.roster_win.selected_row
- if arg:
- jid = arg
+ if args:
+ jid = args[0]
elif isinstance(item, Contact):
jid = item.bare_jid
elif isinstance(item, Resource):
@@ -311,31 +527,45 @@ class RosterInfoTab(Tab):
not self.input.help_message:
self.complete_commands(self.input)
- def completion_file(self, the_input):
+ def completion_file(self, complete_number, the_input):
"""
- Completion for /import and /export
+ Generic quoted completion for files/paths
+ (use functools.partial to use directly as a completion
+ for a command)
"""
text = the_input.get_text()
- args = text.split()
- n = len(args)
- if n == 1:
- home = os.getenv('HOME') or '/'
- return the_input.auto_completion([home, '/tmp'], '')
- else:
- the_path = text[text.index(' ')+1:]
+ args = common.shell_split(text)
+ n = the_input.get_argument_position()
+ if n == complete_number:
+ if args[n-1] == '' or len(args) < n+1:
+ home = os.getenv('HOME') or '/'
+ return the_input.new_completion([home, '/tmp'], n, quotify=True)
+ path_ = args[n]
+ if path.isdir(path_):
+ dir_ = path_
+ base = ''
+ else:
+ dir_ = path.dirname(path_)
+ base = path.basename(path_)
try:
- names = os.listdir(the_path)
- except:
+ names = os.listdir(dir_)
+ except OSError:
names = []
+ names_filtered = [name for name in names if name.startswith(base)]
+ if names_filtered:
+ names = names_filtered
+ if not names:
+ names = [path_]
end_list = []
for name in names:
- value = os.path.join(the_path, name)
+ value = os.path.join(dir_, name)
if not name.startswith('.'):
end_list.append(value)
- return the_input.auto_completion(end_list, '')
+ return the_input.new_completion(end_list, n, quotify=True)
- def command_clear(self, arg=''):
+ @command_args_parser.ignored
+ def command_clear(self):
"""
/clear
"""
@@ -344,7 +574,8 @@ class RosterInfoTab(Tab):
self.core.information_win.rebuild_everything(self.core.information_buffer)
self.refresh()
- def command_password(self, arg):
+ @command_args_parser.quoted(1)
+ def command_password(self, args):
"""
/password <password>
"""
@@ -352,19 +583,18 @@ class RosterInfoTab(Tab):
if iq['type'] == 'result':
self.core.information('Password updated', 'Account')
if config.get('password'):
- config.silent_set('password', arg)
+ config.silent_set('password', args[0])
else:
self.core.information('Unable to change the password', 'Account')
- self.core.xmpp.plugin['xep_0077'].change_password(arg, callback=callback)
-
+ self.core.xmpp.plugin['xep_0077'].change_password(args[0], callback=callback)
-
- def command_deny(self, arg):
+ @command_args_parser.quoted(0, 1)
+ def command_deny(self, args):
"""
/deny [jid]
Denies a JID from our roster
"""
- if not arg:
+ if not args:
item = self.roster_win.selected_row
if isinstance(item, Contact):
jid = item.bare_jid
@@ -372,7 +602,7 @@ class RosterInfoTab(Tab):
self.core.information('No subscription to deny')
return
else:
- jid = safeJID(arg).bare
+ jid = safeJID(args[0]).bare
if not jid in [jid for jid in roster.jids()]:
self.core.information('No subscription to deny')
return
@@ -383,14 +613,15 @@ class RosterInfoTab(Tab):
self.core.information('Subscription to %s was revoked' % jid,
'Roster')
+ @command_args_parser.quoted(1)
def command_add(self, args):
"""
Add the specified JID to the roster, and set automatically
accept the reverse subscription
"""
- jid = safeJID(safeJID(args.strip()).bare)
+ jid = safeJID(safeJID(args[0]).bare)
if not jid:
- self.core.information(_('No JID specified'), 'Error')
+ self.core.information('No JID specified', 'Error')
return
if jid in roster and roster[jid].subscription in ('to', 'both'):
return self.core.information('Already subscribed.', 'Roster')
@@ -398,7 +629,8 @@ class RosterInfoTab(Tab):
roster.modified()
self.core.information('%s was added to the roster' % jid, 'Roster')
- def command_name(self, arg):
+ @command_args_parser.quoted(1, 1)
+ def command_name(self, args):
"""
Set a name for the specified JID in your roster
"""
@@ -406,15 +638,14 @@ class RosterInfoTab(Tab):
if not iq:
self.core.information('The name could not be set.', 'Error')
log.debug('Error in /name:\n%s', iq)
- args = common.shell_split(arg)
- if not args:
+ if args is None:
return self.core.command_help('name')
jid = safeJID(args[0]).bare
name = args[1] if len(args) == 2 else ''
contact = roster[jid]
if contact is None:
- self.core.information(_('No such JID in roster'), 'Error')
+ self.core.information('No such JID in roster', 'Error')
return
groups = set(contact.groups)
@@ -424,24 +655,24 @@ class RosterInfoTab(Tab):
self.core.xmpp.update_roster(jid, name=name, groups=groups,
subscription=subscription, callback=callback)
+ @command_args_parser.quoted(2)
def command_groupadd(self, args):
"""
Add the specified JID to the specified group
"""
- args = common.shell_split(args)
- if len(args) != 2:
- return
+ if args is None:
+ return self.core.command_help('groupadd')
jid = safeJID(args[0]).bare
group = args[1]
contact = roster[jid]
if contact is None:
- self.core.information(_('No such JID in roster'), 'Error')
+ self.core.information('No such JID in roster', 'Error')
return
new_groups = set(contact.groups)
if group in new_groups:
- self.core.information(_('JID already in group'), 'Error')
+ self.core.information('JID already in group', 'Error')
return
roster.modified()
@@ -464,12 +695,12 @@ class RosterInfoTab(Tab):
self.core.xmpp.update_roster(jid, name=name, groups=new_groups,
subscription=subscription, callback=callback)
- def command_groupmove(self, arg):
+ @command_args_parser.quoted(3)
+ def command_groupmove(self, args):
"""
Remove the specified JID from the first specified group and add it to the second one
"""
- args = common.shell_split(arg)
- if len(args) != 3:
+ if args is None:
return self.core.command_help('groupmove')
jid = safeJID(args[0]).bare
group_from = args[1]
@@ -477,7 +708,7 @@ class RosterInfoTab(Tab):
contact = roster[jid]
if not contact:
- self.core.information(_('No such JID in roster'), 'Error')
+ self.core.information('No such JID in roster', 'Error')
return
new_groups = set(contact.groups)
@@ -485,19 +716,19 @@ class RosterInfoTab(Tab):
new_groups.remove('none')
if group_to == 'none' or group_from == 'none':
- self.core.information(_('"none" is not a group.'), 'Error')
+ self.core.information('"none" is not a group.', 'Error')
return
if group_from not in new_groups:
- self.core.information(_('JID not in first group'), 'Error')
+ self.core.information('JID not in first group', 'Error')
return
if group_to in new_groups:
- self.core.information(_('JID already in second group'), 'Error')
+ self.core.information('JID already in second group', 'Error')
return
if group_to == group_from:
- self.core.information(_('The groups are the same.'), 'Error')
+ self.core.information('The groups are the same.', 'Error')
return
roster.modified()
@@ -519,19 +750,20 @@ class RosterInfoTab(Tab):
self.core.xmpp.update_roster(jid, name=name, groups=new_groups,
subscription=subscription, callback=callback)
+ @command_args_parser.quoted(2)
def command_groupremove(self, args):
"""
Remove the specified JID from the specified group
"""
- args = common.shell_split(args)
- if len(args) != 2:
- return
+ if args is None:
+ return self.core.command_help('groupremove')
+
jid = safeJID(args[0]).bare
group = args[1]
contact = roster[jid]
if contact is None:
- self.core.information(_('No such JID in roster'), 'Error')
+ self.core.information('No such JID in roster', 'Error')
return
new_groups = set(contact.groups)
@@ -540,7 +772,7 @@ class RosterInfoTab(Tab):
except KeyError:
pass
if group not in new_groups:
- self.core.information(_('JID not in group'), 'Error')
+ self.core.information('JID not in group', 'Error')
return
roster.modified()
@@ -559,13 +791,14 @@ class RosterInfoTab(Tab):
self.core.xmpp.update_roster(jid, name=name, groups=new_groups,
subscription=subscription, callback=callback)
+ @command_args_parser.quoted(0, 1)
def command_remove(self, args):
"""
Remove the specified JID from the roster. i.e.: unsubscribe
from its presence, and cancel its subscription to our.
"""
- if args.strip():
- jid = safeJID(args.strip()).bare
+ if args:
+ jid = safeJID(args[0]).bare
else:
item = self.roster_win.selected_row
if isinstance(item, Contact):
@@ -576,12 +809,12 @@ class RosterInfoTab(Tab):
roster.remove(jid)
del roster[jid]
- def command_import(self, arg):
+ @command_args_parser.quoted(0, 1)
+ def command_import(self, args):
"""
Import the contacts
"""
- args = common.shell_split(arg)
- if len(args):
+ if args:
if args[0].startswith('/'):
filepath = args[0]
else:
@@ -603,12 +836,12 @@ class RosterInfoTab(Tab):
self.command_add(jid.lstrip('\n'))
self.core.information('Contacts imported from %s' % filepath, 'Info')
- def command_export(self, arg):
+ @command_args_parser.quoted(0, 1)
+ def command_export(self, args):
"""
Export the contacts
"""
- args = common.shell_split(arg)
- if len(args):
+ if args:
if args[0].startswith('/'):
filepath = args[0]
else:
@@ -697,11 +930,12 @@ class RosterInfoTab(Tab):
if contact.pending_in)
return the_input.new_completion(jids, 1, '', quotify=False)
- def command_accept(self, arg):
+ @command_args_parser.quoted(0, 1)
+ def command_accept(self, args):
"""
Accept a JID from in roster. Authorize it AND subscribe to it
"""
- if not arg:
+ if not args:
item = self.roster_win.selected_row
if isinstance(item, Contact):
jid = item.bare_jid
@@ -709,7 +943,7 @@ class RosterInfoTab(Tab):
self.core.information('No subscription to accept')
return
else:
- jid = safeJID(arg).bare
+ jid = safeJID(args[0]).bare
nodepart = safeJID(jid).user
jid = safeJID(jid)
# crappy transports putting resources inside the node part
@@ -769,13 +1003,15 @@ class RosterInfoTab(Tab):
success = config.silent_set(option, str(not value))
roster.modified()
if not success:
- self.core.information(_('Unable to write in the config file'), 'Error')
+ self.core.information('Unable to write in the config file', 'Error')
return True
def on_slash(self):
"""
'/' is pressed, we enter "input mode"
"""
+ if isinstance(self.input, windows.YesNoInput):
+ return
curses.curs_set(1)
self.input = windows.CommandInput("", self.reset_help_message, self.execute_slash_command)
self.input.resize(1, self.width, self.height-1, 0)
@@ -951,6 +1187,8 @@ class RosterInfoTab(Tab):
Start the search. The input should appear with a short instruction
in it.
"""
+ if isinstance(self.input, windows.YesNoInput):
+ return
curses.curs_set(1)
self.input = windows.CommandInput("[Search]", self.on_search_terminate, self.on_search_terminate, self.set_roster_filter)
self.input.resize(1, self.width, self.height-1, 0)
@@ -961,6 +1199,8 @@ class RosterInfoTab(Tab):
@refresh_wrapper.always
def start_search_slow(self):
+ if isinstance(self.input, windows.YesNoInput):
+ return
curses.curs_set(1)
self.input = windows.CommandInput("[Search]", self.on_search_terminate, self.on_search_terminate, self.set_roster_filter_slow)
self.input.resize(1, self.width, self.height-1, 0)
diff --git a/src/tabs/xmltab.py b/src/tabs/xmltab.py
index 083e97c5..6899cd6f 100644
--- a/src/tabs/xmltab.py
+++ b/src/tabs/xmltab.py
@@ -5,52 +5,104 @@ in order to only show the relevant ones, and it can also be frozen or
unfrozen on demand so that the relevant information is not drowned by
the traffic.
"""
-from gettext import gettext as _
-
import logging
log = logging.getLogger(__name__)
import curses
import os
from slixmpp.xmlstream import matcher
-from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.tostring import tostring
+from slixmpp.xmlstream.stanzabase import ElementBase
+from xml.etree import ElementTree as ET
from . import Tab
+import text_buffer
import windows
from xhtml import clean_text
+from decorators import command_args_parser
+from common import safeJID
+
+
+class MatchJID(object):
+
+ def __init__(self, jid, dest=''):
+ self.jid = jid
+ self.dest = dest
+
+ def match(self, xml):
+ from_ = safeJID(xml['from'])
+ to_ = safeJID(xml['to'])
+ if self.jid.full == self.jid.bare:
+ from_ = from_.bare
+ to_ = to_.bare
+
+ if self.dest == 'from':
+ return from_ == self.jid
+ elif self.dest == 'to':
+ return to_ == self.jid
+ return self.jid in (from_, to_)
+
+ def __repr__(self):
+ return '%s%s%s' % (self.dest, ': ' if self.dest else '', self.jid)
+
+MATCHERS_MAPPINGS = {
+ MatchJID: ('JID', lambda obj: repr(obj)),
+ matcher.MatcherId: ('ID', lambda obj: obj._criteria),
+ matcher.MatchXMLMask: ('XMLMask', lambda obj: tostring(obj._criteria)),
+ matcher.MatchXPath: ('XPath', lambda obj: obj._criteria)
+}
class XMLTab(Tab):
def __init__(self):
Tab.__init__(self)
self.state = 'normal'
self.name = 'XMLTab'
- self.text_win = windows.TextWin()
- self.core.xml_buffer.add_window(self.text_win)
+ self.filters = []
+
+ self.core_buffer = self.core.xml_buffer
+ self.filtered_buffer = text_buffer.TextBuffer()
+
self.info_header = windows.XMLInfoWin()
+ self.text_win = windows.XMLTextWin()
+ self.core_buffer.add_window(self.text_win)
self.default_help_message = windows.HelpText("/ to enter a command")
+
self.register_command('close', self.close,
- shortdesc=_("Close this tab."))
+ shortdesc="Close this tab.")
self.register_command('clear', self.command_clear,
- shortdesc=_('Clear the current buffer.'))
+ shortdesc='Clear the current buffer.')
self.register_command('reset', self.command_reset,
- shortdesc=_('Reset the stanza filter.'))
+ shortdesc='Reset the stanza filter.')
self.register_command('filter_id', self.command_filter_id,
usage='<id>',
- desc=_('Show only the stanzas with the id <id>.'),
- shortdesc=_('Filter by id.'))
+ desc='Show only the stanzas with the id <id>.',
+ shortdesc='Filter by id.')
self.register_command('filter_xpath', self.command_filter_xpath,
usage='<xpath>',
- desc=_('Show only the stanzas matching the xpath <xpath>.'),
- shortdesc=_('Filter by XPath.'))
+ desc='Show only the stanzas matching the xpath <xpath>.'
+ ' Any occurrences of %n will be replaced by jabber:client.',
+ shortdesc='Filter by XPath.')
+ self.register_command('filter_jid', self.command_filter_jid,
+ usage='<jid>',
+ desc='Show only the stanzas matching the jid <jid> in from= or to=.',
+ shortdesc='Filter by JID.')
+ self.register_command('filter_from', self.command_filter_from,
+ usage='<jid>',
+ desc='Show only the stanzas matching the jid <jid> in from=.',
+ shortdesc='Filter by JID from.')
+ self.register_command('filter_to', self.command_filter_to,
+ usage='<jid>',
+ desc='Show only the stanzas matching the jid <jid> in to=.',
+ shortdesc='Filter by JID to.')
self.register_command('filter_xmlmask', self.command_filter_xmlmask,
- usage=_('<xml mask>'),
- desc=_('Show only the stanzas matching the given xml mask.'),
- shortdesc=_('Filter by xml mask.'))
+ usage='<xml mask>',
+ desc='Show only the stanzas matching the given xml mask.',
+ shortdesc='Filter by xml mask.')
self.register_command('dump', self.command_dump,
- usage=_('<filename>'),
- desc=_('Writes the content of the XML buffer into a file.'),
- shortdesc=_('Write in a file.'))
+ usage='<filename>',
+ desc='Writes the content of the XML buffer into a file.',
+ shortdesc='Write in a file.')
self.input = self.default_help_message
self.key_func['^T'] = self.close
self.key_func['^I'] = self.completion
@@ -63,6 +115,34 @@ class XMLTab(Tab):
self.filter_type = ''
self.filter = ''
+ def gen_filter_repr(self):
+ if not self.filters:
+ self.filter_type = ''
+ self.filter = ''
+ return
+ filter_types = map(lambda x: MATCHERS_MAPPINGS[type(x)][0], self.filters)
+ filter_strings = map(lambda x: MATCHERS_MAPPINGS[type(x)][1](x), self.filters)
+ self.filter_type = ','.join(filter_types)
+ self.filter = ','.join(filter_strings)
+
+ def update_filters(self, matcher):
+ if not self.filters:
+ messages = self.core_buffer.messages[:]
+ self.filtered_buffer.messages = []
+ self.core_buffer.del_window(self.text_win)
+ self.filtered_buffer.add_window(self.text_win)
+ else:
+ messages = self.filtered_buffer.messages
+ self.filtered_buffer.messages = []
+ self.filters.append(matcher)
+ new_messages = []
+ for msg in messages:
+ if self.match_stanza(ElementBase(ET.fromstring(clean_text(msg.txt)))):
+ new_messages.append(msg)
+ self.filtered_buffer.messages = new_messages
+ self.text_win.rebuild_everything(self.filtered_buffer)
+ self.gen_filter_repr()
+
def on_freeze(self):
"""
Freeze the display.
@@ -70,58 +150,94 @@ class XMLTab(Tab):
self.text_win.toggle_lock()
self.refresh()
- def command_filter_xmlmask(self, arg):
+ def match_stanza(self, stanza):
+ for matcher in self.filters:
+ if not matcher.match(stanza):
+ return False
+ return True
+
+ @command_args_parser.raw
+ def command_filter_xmlmask(self, mask):
"""/filter_xmlmask <xml mask>"""
try:
- handler = Callback('custom matcher', matcher.MatchXMLMask(arg),
- self.core.incoming_stanza)
- self.core.xmpp.remove_handler('custom matcher')
- self.core.xmpp.register_handler(handler)
- self.filter_type = "XML Mask Filter"
- self.filter = arg
+ self.update_filters(matcher.MatchXMLMask(mask))
self.refresh()
- except:
- self.core.information('Invalid XML Mask', 'Error')
+ except Exception as e:
+ self.core.information('Invalid XML Mask: %s' % e, 'Error')
self.command_reset('')
- def command_filter_id(self, arg):
+ @command_args_parser.raw
+ def command_filter_to(self, jid):
+ """/filter_jid_to <jid>"""
+ jid_obj = safeJID(jid)
+ if not jid_obj:
+ return self.core.information('Invalid JID: %s' % jid, 'Error')
+
+ self.update_filters(MatchJID(jid_obj, dest='to'))
+ self.refresh()
+
+ @command_args_parser.raw
+ def command_filter_from(self, jid):
+ """/filter_jid_from <jid>"""
+ jid_obj = safeJID(jid)
+ if not jid_obj:
+ return self.core.information('Invalid JID: %s' % jid, 'Error')
+
+ self.update_filters(MatchJID(jid_obj, dest='from'))
+ self.refresh()
+
+ @command_args_parser.raw
+ def command_filter_jid(self, jid):
+ """/filter_jid <jid>"""
+ jid_obj = safeJID(jid)
+ if not jid_obj:
+ return self.core.information('Invalid JID: %s' % jid, 'Error')
+
+ self.update_filters(MatchJID(jid_obj))
+ self.refresh()
+
+ @command_args_parser.quoted(1)
+ def command_filter_id(self, args):
"""/filter_id <id>"""
- self.core.xmpp.remove_handler('custom matcher')
- handler = Callback('custom matcher', matcher.MatcherId(arg),
- self.core.incoming_stanza)
- self.core.xmpp.register_handler(handler)
- self.filter_type = "Id Filter"
- self.filter = arg
+ if args is None:
+ return self.core.command_help('filter_id')
+
+ self.update_filters(matcher.MatcherId(args[0]))
self.refresh()
- def command_filter_xpath(self, arg):
+ @command_args_parser.raw
+ def command_filter_xpath(self, xpath):
"""/filter_xpath <xpath>"""
try:
- handler = Callback('custom matcher', matcher.MatchXPath(
- arg.replace('%n', self.core.xmpp.default_ns)),
- self.core.incoming_stanza)
- self.core.xmpp.remove_handler('custom matcher')
- self.core.xmpp.register_handler(handler)
- self.filter_type = "XPath Filter"
- self.filter = arg
+ self.update_filters(matcher.MatchXPath(xpath.replace('%n', self.core.xmpp.default_ns)))
self.refresh()
except:
self.core.information('Invalid XML Path', 'Error')
self.command_reset('')
- def command_reset(self, arg):
+ @command_args_parser.ignored
+ def command_reset(self):
"""/reset"""
- self.core.xmpp.remove_handler('custom matcher')
- self.core.xmpp.register_handler(self.core.all_stanzas)
+ if self.filters:
+ self.filters = []
+ self.filtered_buffer.del_window(self.text_win)
+ self.core_buffer.add_window(self.text_win)
+ self.text_win.rebuild_everything(self.core_buffer)
self.filter_type = ''
self.filter = ''
self.refresh()
- def command_dump(self, arg):
+ @command_args_parser.quoted(1)
+ def command_dump(self, args):
"""/dump <filename>"""
- xml = self.core.xml_buffer.messages[:]
- text = '\n'.join(('%s %s' % (msg.str_time, clean_text(msg.txt)) for msg in xml))
- filename = os.path.expandvars(os.path.expanduser(arg))
+ if args is None:
+ return self.core.command_help('dump')
+ if self.filters:
+ xml = self.filtered_buffer.messages[:]
+ else:
+ xml = self.core_buffer.messages[:]
+ text = '\n'.join(('%s %s %s' % (msg.str_time, msg.nickname, clean_text(msg.txt)) for msg in xml))
+ filename = os.path.expandvars(os.path.expanduser(args[0]))
try:
with open(filename, 'w') as fd:
fd.write(text)
@@ -151,12 +267,17 @@ class XMLTab(Tab):
def on_scroll_down(self):
return self.text_win.scroll_down(self.text_win.height-1)
- def command_clear(self, args):
+ @command_args_parser.ignored
+ def command_clear(self):
"""
/clear
"""
- self.core.xml_buffer.messages = []
- self.text_win.rebuild_everything(self.core.xml_buffer)
+ if self.filters:
+ buffer = self.core_buffer
+ else:
+ buffer = self.filtered_buffer
+ buffer.messages = []
+ self.text_win.rebuild_everything(buffer)
self.refresh()
self.core.doupdate()