diff options
Diffstat (limited to 'src/tabs')
-rw-r--r-- | src/tabs/__init__.py | 1 | ||||
-rw-r--r-- | src/tabs/adhoc_commands_list.py | 6 | ||||
-rw-r--r-- | src/tabs/basetabs.py | 124 | ||||
-rw-r--r-- | src/tabs/bookmarkstab.py | 145 | ||||
-rw-r--r-- | src/tabs/conversationtab.py | 73 | ||||
-rw-r--r-- | src/tabs/listtab.py | 6 | ||||
-rw-r--r-- | src/tabs/muclisttab.py | 8 | ||||
-rw-r--r-- | src/tabs/muctab.py | 674 | ||||
-rw-r--r-- | src/tabs/privatetab.py | 44 | ||||
-rw-r--r-- | src/tabs/rostertab.py | 546 | ||||
-rw-r--r-- | src/tabs/xmltab.py | 225 |
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() |