diff --git a/src/core.py b/src/core.py index 9159bfe0..029179de 100644 --- a/src/core.py +++ b/src/core.py @@ -313,44 +313,6 @@ class Core(object): self.plugin_manager.load(plugin) self.plugins_autoloaded = True - def validate_ssl(self, pem): - """ - Check the server certificate using the sleekxmpp ssl_cert event - """ - if config.get('ignore_certificate', 'false').lower() == 'true': - return - cert = config.get('certificate', '') - der = ssl.PEM_cert_to_DER_cert(pem) - found_cert = sha1(der).hexdigest() - if cert: - if found_cert == cert: - log.debug('Cert %s OK', found_cert) - return - else: - saved_input = self.current_tab().input - log.debug('\nWARNING: CERTIFICATE CHANGED old: %s, new: %s\n', cert, found_cert) - input = windows.YesNoInput(text="WARNING! Certificate hash changed to %s. Accept? (y/n)" % found_cert) - self.current_tab().input = input - input.resize(1, self.current_tab().width, self.current_tab().height-1, 0) - input.refresh() - self.doupdate() - self.paused = True - while input.value is None: - pass - self.current_tab().input = saved_input - self.paused = False - if input.value: - self.information('Setting new certificate: old: %s, new: %s' % (cert, found_cert), 'Info') - log.debug('Setting certificate to %s', found_cert) - config.set_and_save('certificate', found_cert) - else: - self.information('You refused to validate the certificate. You are now disconnected', 'Info') - self.xmpp.disconnect() - else: - log.debug('First time. Setting certificate to %s', found_cert) - config.set_and_save('certificate', found_cert) - - def start(self): """ Init curses, create the first tab, etc @@ -371,53 +333,6 @@ class Core(object): )) self.refresh_window() - def resize_global_information_win(self): - """ - Resize the global_information_win only once at each resize. - """ - with g_lock: - self.information_win.resize(self.information_win_size, tabs.Tab.width, - tabs.Tab.height - 1 - self.information_win_size - tabs.Tab.tab_win_height(), 0) - - def outgoing_stanza(self, stanza): - """ - We are sending a new stanza, write it in the xml buffer if needed. - """ - if self.xml_tabs: - self.add_message_to_text_buffer(self.xml_buffer, '\x191}<--\x19o %s' % stanza) - if isinstance(self.current_tab(), tabs.XMLTab): - self.current_tab().refresh() - self.doupdate() - - def incoming_stanza(self, stanza): - """ - We are receiving a new stanza, write it in the xml buffer if needed. - """ - if self.xml_tabs: - self.add_message_to_text_buffer(self.xml_buffer, '\x192}-->\x19o %s' % stanza) - if isinstance(self.current_tab(), tabs.XMLTab): - self.current_tab().refresh() - self.doupdate() - - def command_xml_tab(self, arg=''): - """/xml_tab""" - self.xml_tabs += 1 - tab = tabs.XMLTab() - self.add_tab(tab, True) - - def resize_global_info_bar(self): - """ - Resize the GlobalInfoBar only once at each resize - """ - with g_lock: - self.tab_win.resize(1, tabs.Tab.width, tabs.Tab.height - 2, 0) - if config.get('enable_vertical_tab_list', 'false') == 'true': - height, width = self.stdscr.getmaxyx() - truncated_win = self.stdscr.subwin(height, config.get('vertical_tab_list_size', 20), 0, 0) - self.left_tab_win = windows.VerticalGlobalInfoBar(truncated_win) - else: - self.left_tab_win = None - def on_exception(self, typ, value, trace): """ When an exception is raised, just reset curses and call @@ -429,10 +344,635 @@ class Core(object): pass sys.__excepthook__(typ, value, trace) + def main_loop(self): + """ + main loop waiting for the user to press a key + """ + def replace_line_breaks(key): + if key == '^J': + return '\n' + return key + def replace_tabulation(key): + if key == '^I': + return ' ' + return key + while self.running: + if self.paused: continue + char_list = [common.replace_key_with_bound(key)\ + for key in self.read_keyboard()] + if self.paused: + self.current_tab().input.do_command(char_list[0]) + self.current_tab().input.prompt() + continue + # Special case for M-x where x is a number + if len(char_list) == 1: + char = char_list[0] + if char.startswith('M-') and len(char) == 3: + try: + nb = int(char[2]) + except ValueError: + pass + else: + if self.current_tab().nb == nb: + self.go_to_previous_tab() + else: + self.command_win('%d' % nb) + # search for keyboard shortcut + func = self.key_func.get(char, None) + if func: + func() + else: + res = self.do_command(replace_line_breaks(char), False) + if res: + self.refresh_window() + else: + self.do_command(''.join(map( + lambda x: replace_line_breaks(replace_tabulation(x)), + char_list) + ), True) + self.refresh_window() + self.doupdate() + + def save_config(self): + """ + Save config in the file just before exit + """ + roster.save_to_config_file() + config.set_and_save('info_win_height', self.information_win_size, 'var') + + def on_roster_enter_key(self, roster_row): + """ + when enter is pressed on the roster window + """ + if isinstance(roster_row, Contact): + if not self.get_conversation_by_jid(roster_row.bare_jid): + self.open_conversation_window(roster_row.bare_jid) + else: + self.focus_tab_named(roster_row.bare_jid) + if isinstance(roster_row, Resource): + if not self.get_conversation_by_jid(roster_row.jid.full): + self.open_conversation_window(roster_row.jid.full) + else: + self.focus_tab_named(roster_row.jid.full) + self.refresh_window() + + def get_conversation_messages(self): + """ + Returns a list of all the messages in the current chat. + If the current tab is not a ChatTab, returns None. + + Messages are namedtuples of the form + ('txt nick_color time str_time nickname user') + """ + if not isinstance(self.current_tab(), tabs.ChatTab): + return None + return self.current_tab().get_conversation_messages() + + def insert_input_text(self, text): + """ + Insert the given text into the current input + """ + self.do_command(text, True) + + +##################### Anything related to command execution ################### + + def execute(self, line): + """ + Execute the /command or just send the line on the current room + """ + if line == "": + return + if line.startswith('/'): + command = line.strip()[:].split()[0][1:] + arg = line[2+len(command):] # jump the '/' and the ' ' + # example. on "/link 0 open", command = "link" and arg = "0 open" + if command in self.commands: + func = self.commands[command][0] + func(arg) + return + else: + self.information(_("Unknown command (%s)") % (command), _('Error')) + + def exec_command(self, command): + """ + Execute an external command on the local or a remote + machine, depending on the conf. For example, to open a link in a + browser, do exec_command("firefox http://poezio.eu"), + and this will call the command on the correct computer. + The remote execution is done by writing the command on a fifo. + That fifo has to be on the machine where poezio is running, and + accessible (through sshfs for example) from the local machine (where + poezio is not running). A very simple daemon reads on that fifo, + and executes any command that is read in it. + """ + command = '%s\n' % (command,) + if config.get('exec_remote', 'false') == 'true': + # We just write the command in the fifo + if not self.remote_fifo: + try: + self.remote_fifo = Fifo(os.path.join(config.get('remote_fifo_path', './'), 'poezio.fifo'), 'w') + except (OSError, IOError) as e: + self.information('Could not open fifo file for writing: %s' % (e,), 'Error') + return + try: + self.remote_fifo.write(command) + except (IOError) as e: + self.information('Could not execute [%s]: %s' % (command.strip(), e,), 'Error') + self.remote_fifo = None + else: + e = Executor(command.strip()) + try: + e.start() + except ValueError as e: # whenever shlex fails + self.information('%s' % (e,), 'Error') + + + def do_command(self, key, raw): + if not key: + return + return self.current_tab().on_input(key, raw) + + + def try_execute(self, line): + """ + Try to execute a command in the current tab + """ + line = '/' + line + try: + self.current_tab().execute_command(line) + except: + import traceback + log.debug('Execute failed:\n%s', traceback.format_exc()) + + +########################## TImed Events ####################################### + + def remove_timed_event(self, event): + """Remove an existing timed event""" + if event and event in self.timed_events: + self.timed_events.remove(event) + + def add_timed_event(self, event): + """Add a new timed event""" + self.timed_events.add(event) + + def check_timed_events(self): + """Check for the execution of timed events""" + now = datetime.now() + for event in self.timed_events: + if event.has_timed_out(now): + res = event() + if not res: + self.timed_events.remove(event) + break + + +####################### XMPP-related actions ################################## + + def get_status(self): + """ + Get the last status that was previously set + """ + return self.status + + def set_status(self, pres, msg): + """ + Set our current status so we can remember + it and use it back when needed (for example to display it + or to use it when joining a new muc) + """ + self.status = Status(show=pres, message=msg) + + def get_bookmark_nickname(self, room_name): + """ + Returns the nickname associated with a bookmark + or the default nickname + """ + bm = bookmark.get_by_jid(room_name) + if bm: + return bm.nick + return self.own_nick + + def disconnect(self, msg='', reconnect=False): + """ + Disconnect from remote server and correctly set the states of all + parts of the client (for example, set the MucTabs as not joined, etc) + """ + msg = msg or '' + for tab in self.tabs: + if isinstance(tab, tabs.MucTab) and tab.joined: + tab.command_part(msg) + self.save_config() + # Ugly fix thanks to gmail servers + self.xmpp.disconnect(reconnect) + + def send_message(self, msg): + """ + Function to use in plugins to send a message in the current conversation. + Returns False if the current tab is not a conversation tab + """ + if not isinstance(self.current_tab(), tabs.ChatTab): + return False + self.current_tab().command_say(msg) + return True + + def get_error_message(self, stanza, deprecated=False): + """ + Takes a stanza of the form + and return a well formed string containing the error informations + """ + sender = stanza.attrib['from'] + msg = stanza['error']['type'] + condition = stanza['error']['condition'] + code = stanza['error']['code'] + body = stanza['error']['text'] + if not body: + if deprecated: + if code in DEPRECATED_ERRORS: + body = DEPRECATED_ERRORS[code] + else: + body = condition or _('Unknown error') + else: + if code in ERROR_AND_STATUS_CODES: + body = ERROR_AND_STATUS_CODES[code] + else: + body = condition or _('Unknown error') + if code: + message = _('%(from)s: %(code)s - %(msg)s: %(body)s') % {'from':sender, 'msg':msg, 'body':body, 'code':code} + else: + message = _('%(from)s: %(msg)s: %(body)s') % {'from':sender, 'msg':msg, 'body':body} + return message + + +####################### Tab logic-related things ############################## + + ### Tab getters ### + + def current_tab(self): + """ + returns the current room, the one we are viewing + """ + return self.tabs[0] + + def get_tab_of_conversation_with_jid(self, jid, create=True): + """ + From a JID, get the tab containing the conversation with it. + If none already exist, and create is "True", we create it + and return it. Otherwise, we return None + """ + # We first check if we have a conversation opened with this precise resource + conversation = self.get_tab_by_name(jid.full, tabs.ConversationTab) + if not conversation: + # If not, we search for a conversation with the bare jid + conversation = self.get_tab_by_name(jid.bare, tabs.ConversationTab) + if not conversation: + if create: + # We create the conversation with the full Jid if nothing was found + conversation = self.open_conversation_window(jid.full, False) + else: + conversation = None + return conversation + + def get_conversation_by_jid(self, jid): + """ + Return the room of the ConversationTab with the given jid + """ + for tab in self.tabs: + if isinstance(tab, tabs.ConversationTab): + if tab.get_name() == jid: + return tab + return None + + def get_tab_by_name(self, name, typ=None): + """ + Get the tab with the given name. + If typ is provided, return a tab of this type only + """ + for tab in self.tabs: + if tab.get_name() == name: + if (typ and isinstance(tab, typ)) or\ + not typ: + return tab + return None + + def get_tab_by_number(self, number): + for tab in self.tabs: + if tab.nb == number: + return tab + return None + + def add_tab(self, new_tab, focus=False): + """ + Appends the new_tab in the tab list and + focus it if focus==True + """ + if self.current_tab().nb == 0: + self.tabs.append(new_tab) + else: + for ta in self.tabs: + if ta.nb == 0: + self.tabs.insert(self.tabs.index(ta), new_tab) + break + if focus: + self.command_win("%s" % new_tab.nb) + + ### Move actions (e.g. go to next room) ### + + def rotate_rooms_right(self, args=None): + """ + rotate the rooms list to the right + """ + self.current_tab().on_lose_focus() + self.tabs.append(self.tabs.pop(0)) + self.current_tab().on_gain_focus() + self.refresh_window() + + def rotate_rooms_left(self, args=None): + """ + rotate the rooms list to the right + """ + self.current_tab().on_lose_focus() + self.tabs.insert(0, self.tabs.pop()) + self.current_tab().on_gain_focus() + self.refresh_window() + + def go_to_room_number(self): + """ + Read 2 more chars and go to the tab + with the given number + """ + char = self.read_keyboard()[0] + try: + nb1 = int(char) + except ValueError: + return + char = self.read_keyboard()[0] + try: + nb2 = int(char) + except ValueError: + return + self.command_win('%s%s' % (nb1, nb2)) + + def go_to_roster(self): + self.command_win('0') + + def go_to_previous_tab(self): + self.command_win('%s' % (self.previous_tab_nb,)) + + def go_to_important_room(self): + """ + Go to the next room with activity, in the order defined in the + dict tabs.STATE_PRIORITY + """ + priority = tabs.STATE_PRIORITY + sorted_tabs = sorted(self.tabs, key=lambda tab: priority[tab.state], + reverse=True) + tab = sorted_tabs.pop(0) if sorted_tabs else None + if priority[tab.state] < 0 or not tab: + return + self.command_win('%s' % tab.nb) + + def focus_tab_named(self, tab_name): + for tab in self.tabs: + if tab.get_name() == tab_name: + self.command_win('%s' % (tab.nb,)) + + ### Opening actions ### + + def open_conversation_window(self, jid, focus=True): + """ + Open a new conversation tab and focus it if needed + """ + for tab in self.tabs: # if the room exists, focus it and return + if isinstance(tab, tabs.ConversationTab): + if tab.get_name() == jid: + self.command_win('%s' % tab.nb) + return tab + new_tab = tabs.ConversationTab(jid) + # insert it in the rooms + if not focus: + new_tab.state = "private" + self.add_tab(new_tab, focus) + self.refresh_window() + return new_tab + + def open_private_window(self, room_name, user_nick, focus=True): + """ + Open a Private conversation in a MUC and focus if needed. + """ + complete_jid = room_name+'/'+user_nick + for tab in self.tabs: # if the room exists, focus it and return + if isinstance(tab, tabs.PrivateTab): + if tab.get_name() == complete_jid: + self.command_win('%s' % tab.nb) + return tab + # create the new tab + tab = self.get_tab_by_name(room_name, tabs.MucTab) + if not tab: + return None + new_tab = tabs.PrivateTab(complete_jid, tab.own_nick) + if hasattr(tab, 'directed_presence'): + new_tab.directed_presence = tab.directed_presence + if not focus: + new_tab.state = "private" + # insert it in the tabs + self.add_tab(new_tab, focus) + self.refresh_window() + tab.privates.append(new_tab) + return new_tab + + def open_new_room(self, room, nick, focus=True): + """ + Open a new tab.MucTab containing a muc Room, using the specified nick + """ + new_tab = tabs.MucTab(room, nick) + self.add_tab(new_tab, focus) + self.refresh_window() + + def open_new_form(self, form, on_cancel, on_send, **kwargs): + """ + Open a new tab containing the form + The callback are called with the completed form as parameter in + addition with kwargs + """ + form_tab = DataFormsTab(form, on_cancel, on_send, kwargs) + self.add_tab(form_tab, True) + + ### Modifying actions ### + def rename_private_tabs(self, room_name, old_nick, new_nick): + """ + Call this method when someone changes his/her nick in a MUC, this updates + the name of all the opened private conversations with him/her + """ + tab = self.get_tab_by_name('%s/%s' % (room_name, old_nick), tabs.PrivateTab) + if tab: + tab.rename_user(old_nick, new_nick) + self.on_user_rejoined_private_conversation(room_name, new_nick) + + def on_user_left_private_conversation(self, room_name, nick, status_message): + """ + The user left the MUC: add a message in the associated private conversation + """ + tab = self.get_tab_by_name('%s/%s' % (room_name, nick), tabs.PrivateTab) + if tab: + tab.user_left(status_message, nick) + + def on_user_rejoined_private_conversation(self, room_name, nick): + """ + The user joined a MUC: add a message in the associated private conversation + """ + tab = self.get_tab_by_name('%s/%s' % (room_name, nick), tabs.PrivateTab) + if tab: + tab.user_rejoined(nick) + + def disable_private_tabs(self, room_name, reason='\x195}You left the chatroom\x193}'): + """ + Disable private tabs when leaving a room + """ + for tab in self.tabs: + if tab.get_name().startswith(room_name) and isinstance(tab, tabs.PrivateTab): + tab.deactivate(reason=reason) + + def enable_private_tabs(self, room_name, reason='\x195}You joined the chatroom\x193}'): + """ + Enable private tabs when joining a room + """ + for tab in self.tabs: + if tab.get_name().startswith(room_name) and isinstance(tab, tabs.PrivateTab): + tab.activate(reason=reason) + + def on_user_changed_status_in_private(self, jid, msg): + tab = self.get_tab_by_name(jid) + if tab: # display the message in private + tab.add_message(msg) + + def close_tab(self, tab=None): + """ + Close the given tab. If None, close the current one + """ + tab = tab or self.current_tab() + if isinstance(tab, tabs.RosterInfoTab): + return # The tab 0 should NEVER be closed + del tab.key_func # Remove self references + del tab.commands # and make the object collectable + tab.on_close() + self.tabs.remove(tab) + if tab.get_name() in logger.fds: + logger.fds[tab.get_name()].close() + log.debug("Log file for %s closed.", tab.get_name()) + del logger.fds[tab.get_name()] + self.tabs[0].on_gain_focus() + self.refresh_window() + import gc + gc.collect() + log.debug('___ Referrers of closing tab:\n%s\n______', gc.get_referrers(tab)) + del tab + + def add_information_message_to_conversation_tab(self, jid, msg): + """ + Search for a ConversationTab with the given jid (full or bare), if yes, add + the given message to it + """ + tab = self.get_tab_by_name(jid, tabs.ConversationTab) + if tab: + self.add_message_to_text_buffer(tab._text_buffer, msg) + + +####################### Curses and ui-related stuff ########################### + + def doupdate(self): + if not self.running or self.background is True: + return + curses.doupdate() + + def information(self, msg, typ=''): + """ + Displays an informational message in the "Info" buffer + """ + nb_lines = self.information_buffer.add_message(msg, nickname=typ) + if isinstance(self.current_tab(), tabs.RosterInfoTab): + self.refresh_window() + elif typ != '' and typ.lower() in config.get('information_buffer_popup_on', + 'error roster warning help info').split(): + popup_time = config.get('popup_time', 4) + (nb_lines - 1) * 2 + self.pop_information_win_up(nb_lines, popup_time) + else: + if self.information_win_size != 0: + self.information_win.refresh() + self.current_tab().input.refresh() + + + def init_curses(self, stdscr): + """ + ncurses initialization + """ + self.background = False # Bool to know if curses can draw + # or be quiet while an other console app is running. + curses.curs_set(1) + curses.noecho() + curses.nonl() + curses.raw() + stdscr.idlok(True) + stdscr.keypad(True) + curses.start_color() + curses.use_default_colors() + theming.reload_theme() + curses.ungetch(" ") # H4X: without this, the screen is + stdscr.getkey() # erased on the first "getkey()" + + def reset_curses(self): + """ + Reset terminal capabilities to what they were before ncurses + init + """ + curses.echo() + curses.nocbreak() + curses.curs_set(1) + curses.endwin() + @property def informations(self): return self.information_buffer + def refresh_window(self): + """ + Refresh everything + """ + self.current_tab().state = 'current' + self.current_tab().refresh() + self.doupdate() + + def refresh_tab_win(self): + self.current_tab().refresh_tab_win() + if self.current_tab().input: + self.current_tab().input.refresh() + self.doupdate() + + def scroll_page_down(self, args=None): + self.current_tab().on_scroll_down() + self.refresh_window() + + def scroll_page_up(self, args=None): + self.current_tab().on_scroll_up() + self.refresh_window() + + def scroll_line_up(self, args=None): + self.current_tab().on_line_up() + self.refresh_window() + + def scroll_line_down(self, args=None): + self.current_tab().on_line_down() + self.refresh_window() + + def scroll_half_up(self, args=None): + self.current_tab().on_half_scroll_up() + self.refresh_window() + + def scroll_half_down(self, args=None): + self.current_tab().on_half_scroll_down() + self.refresh_window() + def grow_information_win(self, nb=1): if self.information_win_size >= self.current_tab().height -5 or \ self.information_win_size+nb >= self.current_tab().height-4: @@ -496,566 +1036,36 @@ class Core(object): config.set('enable_vertical_tab_list', 'false' if enabled == 'true' else 'true') self.call_for_resize() - def get_status(self): + def resize_global_information_win(self): """ - Get the last status that was previously set + Resize the global_information_win only once at each resize. """ - return self.status + with g_lock: + self.information_win.resize(self.information_win_size, tabs.Tab.width, + tabs.Tab.height - 1 - self.information_win_size - tabs.Tab.tab_win_height(), 0) - def set_status(self, pres, msg): + def resize_global_info_bar(self): """ - Set our current status so we can remember - it and use it back when needed (for example to display it - or to use it when joining a new muc) + Resize the GlobalInfoBar only once at each resize """ - self.status = Status(show=pres, message=msg) - - def on_groupchat_invite(self, message): - jid = message['from'] - if jid.bare in self.pending_invites: - return - # there are 2 'x' tags in the messages, making message['x'] useless - invite = StanzaBase(self.xmpp, xml=message.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite')) - inviter = invite['from'] - reason = invite['reason'] - password = invite['password'] - msg = "You are invited to the room %s by %s" % (jid.full, inviter.full) - if reason: - msg += "because: %s" % reason - if password: - msg += ". The password is \"%s\"." % password - self.information(msg, 'Info') - if 'invite' in config.get('beep_on', 'invite').split(): - curses.beep() - self.pending_invites[jid.bare] = inviter.full - - def command_invite(self, arg): - """/invite [reason]""" - args = common.shell_split(arg) - if len(args) < 2: - return - reason = args[2] if len(args) > 2 else '' - to = args[0] - room = args[1] - self.xmpp.plugin['xep_0045'].invite(room, to, reason) - - def completion_invite(self, the_input): - """Completion for /invite""" - txt = the_input.get_text() - args = common.shell_split(txt) - n = len(args) - if txt.endswith(' '): - n += 1 - if n == 2: - return the_input.auto_completion([jid for jid in roster.jids()], '') - elif n == 3: - rooms = [] - for tab in self.tabs: - if isinstance(tab, tabs.MucTab) and tab.joined: - rooms.append(tab.get_name()) - return the_input.auto_completion(rooms, '') - - def command_decline(self, arg): - """/decline [reason]""" - args = common.shell_split(arg) - if not len(args): - return - jid = JID(args[0]) - if jid.bare not in self.pending_invites: - return - reason = args[1] if len(args) > 1 else '' - del self.pending_invites[jid.bare] - self.xmpp.plugin['xep_0045'].decline_invite(jid.bare, self.pending_invites[jid.bare], reason) - - def completion_decline(self, the_input): - """Completion for /decline""" - txt = the_input.get_text() - args = common.shell_split(txt) - n = len(args) - if txt.endswith(' '): - n += 1 - if n == 2: - return the_input.auto_completion(list(self.pending_invites.keys()), '') - - def command_invitations(self, arg=''): - """/invitations""" - build = "" - for invite in self.pending_invites: - build += "%s by %s" % (invite, JID(self.pending_invites[invite]).bare) - if self.pending_invites: - build = "You are invited to the following rooms:\n" + build - else: - build = "You are do not have any pending invitation." - self.information(build, 'Info') - - def on_groupchat_decline(self, decline): - pass - - def on_data_form(self, message): - """ - When a data form is received - """ - self.information('%s' % message) - - def on_chatstate_active(self, message): - self.on_chatstate(message, "active") - - def on_chatstate_inactive(self, message): - self.on_chatstate(message, "inactive") - - def on_chatstate_composing(self, message): - self.on_chatstate(message, "composing") - - def on_chatstate_paused(self, message): - self.on_chatstate(message, "paused") - - def on_chatstate_gone(self, message): - self.on_chatstate(message, "gone") - - def on_chatstate(self, message, state): - if message['type'] == 'chat': - if not self.on_chatstate_normal_conversation(message, state): - tab = self.get_tab_by_name(message['from'].full, tabs.PrivateTab) - if not tab: - return - self.on_chatstate_private_conversation(message, state) - elif message['type'] == 'groupchat': - self.on_chatstate_groupchat_conversation(message, state) - - def on_chatstate_normal_conversation(self, message, state): - tab = self.get_tab_of_conversation_with_jid(message['from'], False) - if not tab: - return False - self.events.trigger('normal_chatstate', message, tab) - tab.chatstate = state - if tab == self.current_tab(): - tab.refresh_info_header() - self.doupdate() - return True - - def on_chatstate_private_conversation(self, message, state): - """ - Chatstate received in a private conversation from a MUC - """ - tab = self.get_tab_by_name(message['from'].full, tabs.PrivateTab) - if not tab: - return - self.events.trigger('private_chatstate', message, tab) - tab.chatstate = state - if tab == self.current_tab(): - tab.refresh_info_header() - self.doupdate() - return True - - def on_chatstate_groupchat_conversation(self, message, state): - """ - Chatstate received in a MUC - """ - nick = message['mucnick'] - room_from = message.getMucroom() - tab = self.get_tab_by_name(room_from, tabs.MucTab) - if tab and tab.get_user_by_name(nick): - self.events.trigger('muc_chatstate', message, tab) - tab.get_user_by_name(nick).chatstate = state - if tab == self.current_tab(): - tab.user_win.refresh(tab.users) - tab.input.refresh() - self.doupdate() - - def on_attention(self, message): - """ - Attention probe received. - """ - jid_from = message['from'] - self.information('%s requests your attention!' % jid_from, 'Info') - for tab in self.tabs: - if tab.get_name() == jid_from: - tab.state = 'attention' - self.refresh_tab_win() - return - for tab in self.tabs: - if tab.get_name() == jid_from.bare: - tab.state = 'attention' - self.refresh_tab_win() - return - self.information('%s tab not found.' % jid_from, 'Error') - - def open_new_form(self, form, on_cancel, on_send, **kwargs): - """ - Open a new tab containing the form - The callback are called with the completed form as parameter in - addition with kwargs - """ - form_tab = DataFormsTab(form, on_cancel, on_send, kwargs) - self.add_tab(form_tab, True) - - def on_got_offline(self, presence): - """ - A JID got offline - """ - jid = presence['from'] - logger.log_roster_change(jid.bare, 'got offline') - # If a resource got offline, display the message in the conversation with this - # precise resource. - if jid.resource: - self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x191}offline' % (jid.full)) - if jid.server in roster.blacklist: - return - self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x191}offline' % (jid.bare)) - self.information('\x193}%s \x195}is \x191}offline' % (jid.bare), 'Roster') - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() - - def on_got_online(self, presence): - jid = presence['from'] - contact = roster[jid.bare] - if contact is None: - # Todo, handle presence coming from contacts not in roster - return - logger.log_roster_change(jid.bare, 'got online') - resource = Resource(jid.full, { - 'priority': presence.get_priority() or 0, - 'status': presence['status'], - 'show': presence['show'], - }) - self.events.trigger('normal_presence', presence, resource) - self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x194}online' % (jid.full)) - if time.time() - self.connection_time > 20: - # We do not display messages if we recently logged in - if presence['status']: - self.information("\x193}%s \x195}is \x194}online\x195} (\x19o%s\x195})" % (resource.jid.bare, presence['status']), "Roster") + with g_lock: + self.tab_win.resize(1, tabs.Tab.width, tabs.Tab.height - 2, 0) + if config.get('enable_vertical_tab_list', 'false') == 'true': + height, width = self.stdscr.getmaxyx() + truncated_win = self.stdscr.subwin(height, config.get('vertical_tab_list_size', 20), 0, 0) + self.left_tab_win = windows.VerticalGlobalInfoBar(truncated_win) else: - self.information("\x193}%s \x195}is \x194}online\x195}" % resource.jid.bare, "Roster") - self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x194}online' % (jid.bare)) - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() + self.left_tab_win = None - def add_information_message_to_conversation_tab(self, jid, msg): + def add_message_to_text_buffer(self, buff, txt, time=None, nickname=None, history=None): """ - Search for a ConversationTab with the given jid (full or bare), if yes, add - the given message to it + Add the message to the room if possible, else, add it to the Info window + (in the Info tab of the info window in the RosterTab) """ - tab = self.get_tab_by_name(jid, tabs.ConversationTab) - if tab: - self.add_message_to_text_buffer(tab._text_buffer, msg) - - def on_failed_connection(self): - """ - We cannot contact the remote server - """ - self.information(_("Connection to remote server failed")) - - def on_disconnected(self, event): - """ - When we are disconnected from remote server - """ - for tab in self.tabs: - if isinstance(tab, tabs.MucTab): - tab.disconnect() - self.information(_("Disconnected from server.")) - - def on_failed_auth(self, event): - """ - Authentication failed - """ - self.information(_("Authentication failed.")) - - def on_connected(self, event): - """ - Remote host responded, but we are not yet authenticated - """ - self.information(_("Connected to server.")) - - def on_session_start(self, event): - """ - Called when we are connected and authenticated - """ - self.connection_time = time.time() - if not self.plugins_autoloaded: # Do not reload plugins on reconnection - self.autoload_plugins() - self.information(_("Authentication success.")) - self.information(_("Your JID is %s") % self.xmpp.boundjid.full) - if not self.xmpp.anon: - # request the roster - self.xmpp.get_roster() - # send initial presence - if config.get('send_initial_presence', 'true').lower() == 'true': - pres = self.xmpp.make_presence() - self.events.trigger('send_normal_presence', pres) - pres.send() - bookmark.get_local() - if not self.xmpp.anon and not config.get('use_remote_bookmarks', 'true').lower() == 'false': - bookmark.get_remote(self.xmpp) - for bm in [item for item in bookmark.bookmarks if item.autojoin]: - tab = self.get_tab_by_name(bm.jid, tabs.MucTab) - if not tab: - self.open_new_room(bm.jid, bm.nick, False) - nick = bm.nick if bm.nick else self.own_nick - self.initial_joins.append(bm.jid) - muc.join_groupchat(self.xmpp, bm.jid, nick) - - def on_groupchat_presence(self, presence): - """ - Triggered whenever a presence stanza is received from a user in a multi-user chat room. - Display the presence on the room window and update the - presence information of the concerned user - """ - from_room = presence['from'].bare - tab = self.get_tab_by_name(from_room, tabs.MucTab) - if tab: - self.events.trigger('muc_presence', presence, tab) - tab.handle_presence(presence) - - def rename_private_tabs(self, room_name, old_nick, new_nick): - """ - Call this method when someone changes his/her nick in a MUC, this updates - the name of all the opened private conversations with him/her - """ - tab = self.get_tab_by_name('%s/%s' % (room_name, old_nick), tabs.PrivateTab) - if tab: - tab.rename_user(old_nick, new_nick) - self.on_user_rejoined_private_conversation(room_name, new_nick) - - def on_user_left_private_conversation(self, room_name, nick, status_message): - """ - The user left the MUC: add a message in the associated private conversation - """ - tab = self.get_tab_by_name('%s/%s' % (room_name, nick), tabs.PrivateTab) - if tab: - tab.user_left(status_message, nick) - - def on_user_rejoined_private_conversation(self, room_name, nick): - """ - The user joined a MUC: add a message in the associated private conversation - """ - tab = self.get_tab_by_name('%s/%s' % (room_name, nick), tabs.PrivateTab) - if tab: - tab.user_rejoined(nick) - - def disable_private_tabs(self, room_name, reason='\x195}You left the chatroom\x193}'): - """ - Disable private tabs when leaving a room - """ - for tab in self.tabs: - if tab.get_name().startswith(room_name) and isinstance(tab, tabs.PrivateTab): - tab.deactivate(reason=reason) - - def enable_private_tabs(self, room_name, reason='\x195}You joined the chatroom\x193}'): - """ - Enable private tabs when joining a room - """ - for tab in self.tabs: - if tab.get_name().startswith(room_name) and isinstance(tab, tabs.PrivateTab): - tab.activate(reason=reason) - - def on_user_changed_status_in_private(self, jid, msg): - tab = self.get_tab_by_name(jid) - if tab: # display the message in private - tab.add_message(msg) - - def on_message(self, message): - """ - When receiving private message from a muc OR a normal message - (from one of our contacts) - """ - if message.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite') != None: - return - if message['type'] == 'groupchat': - return - # Differentiate both type of messages, and call the appropriate handler. - jid_from = message['from'] - for tab in self.tabs: - if tab.get_name() == jid_from.bare and isinstance(tab, tabs.MucTab): - if message['type'] == 'error': - return self.room_error(message, jid_from) - else: - return self.on_groupchat_private_message(message) - return self.on_normal_message(message) - - def on_groupchat_private_message(self, message): - """ - We received a Private Message (from someone in a Muc) - """ - jid = message['from'] - nick_from = jid.resource - room_from = jid.bare - body = xhtml.get_body_from_message_stanza(message) - tab = self.get_tab_by_name(jid.full, tabs.PrivateTab) # get the tab with the private conversation - ignore = config.get_by_tabname('ignore_private', 'false', - room_from).lower() == 'true' - if not tab: # It's the first message we receive: create the tab - if body and not ignore: - tab = self.open_private_window(room_from, nick_from, False) - if ignore: - self.events.trigger('ignored_private', message, tab) - msg = config.get_by_tabname('private_auto_response', None, room_from) - if msg and body: - self.xmpp.send_message(mto=jid.full, mbody=msg, mtype='chat') - return - self.events.trigger('private_msg', message, tab) - if not body or not tab: - return - tab.add_message(body, time=None, nickname=nick_from, - forced_user=self.get_tab_by_name(room_from, tabs.MucTab).get_user_by_name(nick_from)) - conversation = self.get_tab_by_name(jid.full, tabs.PrivateTab) - if conversation and conversation.remote_wants_chatstates is None: - if message['chat_state']: - conversation.remote_wants_chatstates = True - else: - conversation.remote_wants_chatstates = False - if 'private' in config.get('beep_on', 'highlight private').split(): - if config.get_by_tabname('disable_beep', 'false', jid.full, False).lower() != 'true': - curses.beep() - logger.log_message(jid.full.replace('/', '\\'), nick_from, body) - if conversation is self.current_tab(): - self.refresh_window() + if not buff: + self.information('Trying to add a message in no room: %s' % txt, 'Error') else: - conversation.state = 'private' - self.refresh_tab_win() - - def focus_tab_named(self, tab_name): - for tab in self.tabs: - if tab.get_name() == tab_name: - self.command_win('%s' % (tab.nb,)) - - def get_tab_of_conversation_with_jid(self, jid, create=True): - """ - From a JID, get the tab containing the conversation with it. - If none already exist, and create is "True", we create it - and return it. Otherwise, we return None - """ - # We first check if we have a conversation opened with this precise resource - conversation = self.get_tab_by_name(jid.full, tabs.ConversationTab) - if not conversation: - # If not, we search for a conversation with the bare jid - conversation = self.get_tab_by_name(jid.bare, tabs.ConversationTab) - if not conversation: - if create: - # We create the conversation with the full Jid if nothing was found - conversation = self.open_conversation_window(jid.full, False) - else: - conversation = None - return conversation - - def on_normal_message(self, message): - """ - When receiving "normal" messages (from someone in our roster) - """ - jid = message['from'] - body = xhtml.get_body_from_message_stanza(message) - if message['type'] == 'error': - return self.information(self.get_error_message_from_error_stanza(message, deprecated=True), 'Error') - if not body: - return - conversation = self.get_tab_of_conversation_with_jid(jid, create=True) - self.events.trigger('conversation_msg', message, conversation) - body = xhtml.get_body_from_message_stanza(message) - if jid.bare in roster: - remote_nick = roster[jid.bare].name or jid.user - else: - remote_nick = jid.user - delay_tag = message.find('{urn:xmpp:delay}delay') - if delay_tag is not None: - delayed = True - date = common.datetime_tuple(delay_tag.attrib['stamp']) - else: - delayed = False - date = None - conversation._text_buffer.add_message(body, date, nickname=remote_nick, nick_color=get_theme().COLOR_REMOTE_USER, history=delayed) - if conversation.remote_wants_chatstates is None and not delayed: - if message['chat_state']: - conversation.remote_wants_chatstates = True - else: - conversation.remote_wants_chatstates = False - logger.log_message(jid.bare, remote_nick, body) - if 'private' in config.get('beep_on', 'highlight private').split(): - if config.get_by_tabname('disable_beep', 'false', jid.bare, False).lower() != 'true': - curses.beep() - if self.current_tab() is not conversation: - conversation.state = 'private' - self.refresh_tab_win() - else: - self.refresh_window() - - def on_presence(self, presence): - jid = presence['from'] - contact = roster[jid.bare] - if contact is None: - return - self.events.trigger('normal_presence', presence, contact[jid.full]) - tab = self.get_tab_of_conversation_with_jid(jid, create=False) - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() - elif self.current_tab() == tab: - tab.refresh() - self.doupdate() - - def on_roster_update(self, iq): - """ - The roster was received. - """ - for item in iq['roster']: - jid = item['jid'] - if item['subscription'] == 'remove': - del roster[jid] - else: - roster.update_contact_groups(jid) - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() - - def on_subscription_request(self, presence): - """subscribe received""" - jid = presence['from'].bare - contact = roster[jid] - if contact.subscription in ('from', 'both'): - return - elif contact.subscription == 'to': - self.xmpp.sendPresence(pto=jid, ptype='subscribed') - self.xmpp.sendPresence(pto=jid) - else: - roster.update_contact_groups(contact) - contact.pending_in = True - self.information('%s wants to subscribe to your presence' % jid, 'Roster') - self.get_tab_by_number(0).state = 'highlight' - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() - - def on_subscription_authorized(self, presence): - """subscribed received""" - jid = presence['from'].bare - contact = roster[jid] - if contact.subscription not in ('both', 'from'): - self.information('%s accepted your contact proposal' % jid, 'Roster') - if contact.pending_out: - contact.pending_out = False - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() - - def on_subscription_remove(self, presence): - """unsubscribe received""" - jid = presence['from'].bare - contact = roster[jid] - if not contact: - return - self.information('%s does not want to receive your status anymore.' % jid, 'Roster') - self.get_tab_by_number(0).state = 'highlight' - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() - - def on_subscription_removed(self, presence): - """unsubscribed received""" - jid = presence['from'].bare - contact = roster[jid] - if not contact: - return - if contact.pending_out: - self.information('%s rejected your contact proposal' % jid, 'Roster') - contact.pending_out = False - else: - self.information('%s does not want you to receive his/her/its status anymore.'%jid, 'Roster') - self.get_tab_by_number(0).state = 'highlight' - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() + buff.add_message(txt, time, nickname, history=history) def full_screen_redraw(self): """ @@ -1101,427 +1111,7 @@ class Core(object): res = read_char(self.stdscr) return res - def main_loop(self): - """ - main loop waiting for the user to press a key - """ - def replace_line_breaks(key): - if key == '^J': - return '\n' - return key - def replace_tabulation(key): - if key == '^I': - return ' ' - return key - while self.running: - if self.paused: continue - char_list = [common.replace_key_with_bound(key)\ - for key in self.read_keyboard()] - if self.paused: - self.current_tab().input.do_command(char_list[0]) - self.current_tab().input.prompt() - continue - # Special case for M-x where x is a number - if len(char_list) == 1: - char = char_list[0] - if char.startswith('M-') and len(char) == 3: - try: - nb = int(char[2]) - except ValueError: - pass - else: - if self.current_tab().nb == nb: - self.go_to_previous_tab() - else: - self.command_win('%d' % nb) - # search for keyboard shortcut - func = self.key_func.get(char, None) - if func: - func() - else: - res = self.do_command(replace_line_breaks(char), False) - if res: - self.refresh_window() - else: - self.do_command(''.join(map( - lambda x: replace_line_breaks(replace_tabulation(x)), - char_list) - ), True) - self.refresh_window() - self.doupdate() - - def current_tab(self): - """ - returns the current room, the one we are viewing - """ - return self.tabs[0] - - def get_conversation_by_jid(self, jid): - """ - Return the room of the ConversationTab with the given jid - """ - for tab in self.tabs: - if isinstance(tab, tabs.ConversationTab): - if tab.get_name() == jid: - return tab - return None - - def get_tab_by_name(self, name, typ=None): - """ - Get the tab with the given name. - If typ is provided, return a tab of this type only - """ - for tab in self.tabs: - if tab.get_name() == name: - if (typ and isinstance(tab, typ)) or\ - not typ: - return tab - return None - - def get_tab_by_number(self, number): - for tab in self.tabs: - if tab.nb == number: - return tab - return None - - def init_curses(self, stdscr): - """ - ncurses initialization - """ - self.background = False # Bool to know if curses can draw - # or be quiet while an other console app is running. - curses.curs_set(1) - curses.noecho() - curses.nonl() - curses.raw() - stdscr.idlok(True) - stdscr.keypad(True) - curses.start_color() - curses.use_default_colors() - theming.reload_theme() - curses.ungetch(" ") # H4X: without this, the screen is - stdscr.getkey() # erased on the first "getkey()" - - def reset_curses(self): - """ - Reset terminal capabilities to what they were before ncurses - init - """ - curses.echo() - curses.nocbreak() - curses.curs_set(1) - curses.endwin() - - def refresh_window(self): - """ - Refresh everything - """ - self.current_tab().state = 'current' - self.current_tab().refresh() - self.doupdate() - - def refresh_tab_win(self): - self.current_tab().refresh_tab_win() - if self.current_tab().input: - self.current_tab().input.refresh() - self.doupdate() - - def add_tab(self, new_tab, focus=False): - """ - Appends the new_tab in the tab list and - focus it if focus==True - """ - if self.current_tab().nb == 0: - self.tabs.append(new_tab) - else: - for ta in self.tabs: - if ta.nb == 0: - self.tabs.insert(self.tabs.index(ta), new_tab) - break - if focus: - self.command_win("%s" % new_tab.nb) - - def open_new_room(self, room, nick, focus=True): - """ - Open a new tab.MucTab containing a muc Room, using the specified nick - """ - new_tab = tabs.MucTab(room, nick) - self.add_tab(new_tab, focus) - self.refresh_window() - - def go_to_roster(self): - self.command_win('0') - - def go_to_previous_tab(self): - self.command_win('%s' % (self.previous_tab_nb,)) - - def go_to_important_room(self): - """ - Go to the next room with activity, in the order defined in the - dict tabs.STATE_PRIORITY - """ - priority = tabs.STATE_PRIORITY - sorted_tabs = sorted(self.tabs, key=lambda tab: priority[tab.state], - reverse=True) - tab = sorted_tabs.pop(0) if sorted_tabs else None - if priority[tab.state] < 0 or not tab: - return - self.command_win('%s' % tab.nb) - - def rotate_rooms_right(self, args=None): - """ - rotate the rooms list to the right - """ - self.current_tab().on_lose_focus() - self.tabs.append(self.tabs.pop(0)) - self.current_tab().on_gain_focus() - self.refresh_window() - - def rotate_rooms_left(self, args=None): - """ - rotate the rooms list to the right - """ - self.current_tab().on_lose_focus() - self.tabs.insert(0, self.tabs.pop()) - self.current_tab().on_gain_focus() - self.refresh_window() - - def scroll_page_down(self, args=None): - self.current_tab().on_scroll_down() - self.refresh_window() - - def scroll_page_up(self, args=None): - self.current_tab().on_scroll_up() - self.refresh_window() - - def scroll_line_up(self, args=None): - self.current_tab().on_line_up() - self.refresh_window() - - def scroll_line_down(self, args=None): - self.current_tab().on_line_down() - self.refresh_window() - - def scroll_half_up(self, args=None): - self.current_tab().on_half_scroll_up() - self.refresh_window() - - def scroll_half_down(self, args=None): - self.current_tab().on_half_scroll_down() - self.refresh_window() - - def get_error_message(self, stanza, deprecated=False): - """ - Takes a stanza of the form - and return a well formed string containing the error informations - """ - sender = stanza.attrib['from'] - msg = stanza['error']['type'] - condition = stanza['error']['condition'] - code = stanza['error']['code'] - body = stanza['error']['text'] - if not body: - if deprecated: - if code in DEPRECATED_ERRORS: - body = DEPRECATED_ERRORS[code] - else: - body = condition or _('Unknown error') - else: - if code in ERROR_AND_STATUS_CODES: - body = ERROR_AND_STATUS_CODES[code] - else: - body = condition or _('Unknown error') - if code: - message = _('%(from)s: %(code)s - %(msg)s: %(body)s') % {'from':sender, 'msg':msg, 'body':body, 'code':code} - else: - message = _('%(from)s: %(msg)s: %(body)s') % {'from':sender, 'msg':msg, 'body':body} - return message - - def room_error(self, error, room_name): - """ - Display the error in the tab - """ - tab = self.get_tab_by_name(room_name) - error_message = self.get_error_message(error) - self.add_message_to_text_buffer(tab._text_buffer, error_message) - code = error['error']['code'] - if code == '401': - msg = _('To provide a password in order to join the room, type "/join / password" (replace "password" by the real password)') - self.add_message_to_text_buffer(tab._text_buffer, msg) - if code == '409': - if config.get('alternative_nickname', '') != '': - self.command_join('%s/%s'% (tab.name, tab.own_nick+config.get('alternative_nickname', ''))) - else: - self.add_message_to_text_buffer(tab._text_buffer, _('You can join the room with an other nick, by typing "/join /other_nick"')) - self.refresh_window() - - def open_conversation_window(self, jid, focus=True): - """ - Open a new conversation tab and focus it if needed - """ - for tab in self.tabs: # if the room exists, focus it and return - if isinstance(tab, tabs.ConversationTab): - if tab.get_name() == jid: - self.command_win('%s' % tab.nb) - return tab - new_tab = tabs.ConversationTab(jid) - # insert it in the rooms - if not focus: - new_tab.state = "private" - self.add_tab(new_tab, focus) - self.refresh_window() - return new_tab - - def open_private_window(self, room_name, user_nick, focus=True): - """ - Open a Private conversation in a MUC and focus if needed. - """ - complete_jid = room_name+'/'+user_nick - for tab in self.tabs: # if the room exists, focus it and return - if isinstance(tab, tabs.PrivateTab): - if tab.get_name() == complete_jid: - self.command_win('%s' % tab.nb) - return tab - # create the new tab - tab = self.get_tab_by_name(room_name, tabs.MucTab) - if not tab: - return None - new_tab = tabs.PrivateTab(complete_jid, tab.own_nick) - if hasattr(tab, 'directed_presence'): - new_tab.directed_presence = tab.directed_presence - if not focus: - new_tab.state = "private" - # insert it in the tabs - self.add_tab(new_tab, focus) - self.refresh_window() - tab.privates.append(new_tab) - return new_tab - - def on_groupchat_subject(self, message): - """ - Triggered when the topic is changed. - """ - nick_from = message['mucnick'] - room_from = message.getMucroom() - tab = self.get_tab_by_name(room_from, tabs.MucTab) - subject = message['subject'] - if not subject or not tab: - return - if nick_from: - self.add_message_to_text_buffer(tab._text_buffer, - _("\x19%(info_col)s}%(nick)s set the subject to: %(subject)s") % - {'info_col': get_theme().COLOR_INFORMATION_TEXT[0], 'nick':nick_from, 'subject':subject}, - time=None) - else: - self.add_message_to_text_buffer(tab._text_buffer, _("\x19%(info_col)s}The subject is: %(subject)s") % - {'subject':subject, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}, - time=None) - tab.topic = subject - if self.get_tab_by_name(room_from, tabs.MucTab) is self.current_tab(): - self.refresh_window() - - def on_groupchat_message(self, message): - """ - Triggered whenever a message is received from a multi-user chat room. - """ - if message['subject']: - return - delay_tag = message.find('{urn:xmpp:delay}delay') - if delay_tag is not None: - delayed = True - date = common.datetime_tuple(delay_tag.attrib['stamp']) - else: - # We support the OLD and deprecated XEP: http://xmpp.org/extensions/xep-0091.html - # But it sucks, please, Jabber servers, don't do this :( - delay_tag = message.find('{jabber:x:delay}x') - if delay_tag is not None: - delayed = True - date = common.datetime_tuple(delay_tag.attrib['stamp']) - else: - delayed = False - date = None - nick_from = message['mucnick'] - room_from = message.getMucroom() - if message['type'] == 'error': # Check if it's an error - return self.room_error(message, room_from) - tab = self.get_tab_by_name(room_from, tabs.MucTab) - old_state = tab.state - if not tab: - self.information(_("message received for a non-existing room: %s") % (room_from)) - return - if tab.get_user_by_name(nick_from) and\ - tab.get_user_by_name(nick_from) in tab.ignores: - return - self.events.trigger('muc_msg', message, tab) - body = xhtml.get_body_from_message_stanza(message) - if body: - date = date if delayed == True else None - if tab.add_message(body, date, nick_from, history=True if date else False): - self.events.trigger('highlight', message, tab) - if tab is self.current_tab(): - tab.text_win.refresh() - tab.info_header.refresh(tab, tab.text_win) - tab.input.refresh() - self.doupdate() - elif tab.state != old_state: - self.refresh_tab_win() - self.current_tab().input.refresh() - self.doupdate() - if 'message' in config.get('beep_on', 'highlight private').split(): - if config.get_by_tabname('disable_beep', 'false', room_from, False).lower() != 'true': - curses.beep() - - def on_status_codes(self, message): - """ - Handle groupchat messages with status codes. - Those are received when a room configuration change occurs. - """ - room_from = message['from'] - tab = self.get_tab_by_name(room_from, tabs.MucTab) - status_codes = set([s.attrib['code'] for s in message.findall('{%s}x/{%s}status' % (tabs.NS_MUC_USER, tabs.NS_MUC_USER))]) - if '101' in status_codes: - self.information('Your affiliation in the room %s changed' % room_from, 'Info') - elif tab and status_codes: - show_unavailable = '102' in status_codes - hide_unavailable = '103' in status_codes - non_priv = '104' in status_codes - logging_on = '170' in status_codes - logging_off= '171' in status_codes - non_anon = '172' in status_codes - semi_anon = '173' in status_codes - full_anon = '174' in status_codes - modif = False - if show_unavailable or hide_unavailable or non_priv or logging_off\ - or non_anon or semi_anon or full_anon: - tab.add_message('\x19%(info_col)s}Info: A configuration change not privacy-related occured.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) - modif = True - if show_unavailable: - tab.add_message('\x19%(info_col)s}Info: The unavailable members are now shown.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) - elif hide_unavailable: - tab.add_message('\x19%(info_col)s}Info: The unavailable members are now hidden.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) - if non_anon: - tab.add_message('\x191}Warning:\x19%(info_col)s} The room is now not anonymous. (public JID)' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) - elif semi_anon: - tab.add_message('\x19%(info_col)s}Info: The room is now semi-anonymous. (moderators-only JID)' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) - elif full_anon: - tab.add_message('\x19%(info_col)s}Info: The room is now fully anonymous.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) - if logging_on: - tab.add_message('\x191}Warning: \x19%(info_col)s}This room is publicly logged' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) - elif logging_off: - tab.add_message('\x19%(info_col)s}Info: This room is not logged anymore.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) - if modif: - self.refresh_window() - - - def add_message_to_text_buffer(self, buff, txt, time=None, nickname=None, history=None): - """ - Add the message to the room if possible, else, add it to the Info window - (in the Info tab of the info window in the RosterTab) - """ - if not buff: - self.information('Trying to add a message in no room: %s' % txt, 'Error') - else: - buff.add_message(txt, time, nickname, history=history) +####################### Commands and completions ############################## def command_help(self, arg): """ @@ -1582,19 +1172,11 @@ class Core(object): if isinstance(current, tabs.MucTab) and current.joined and show not in ('away', 'xa'): current.send_chat_state('active') - def command_rawxml(self, arg): + def completion_status(self, the_input): """ - /rawxml + Completion of /status """ - if not arg: - return - - try: - StanzaBase(self.xmpp, xml=ET.fromstring(arg)).send() - except: - import traceback - self.information(_('Could not send custom stanza'), 'Error') - log.debug(_("Could not send custom stanza:\n") + traceback.format_exc()) + return the_input.auto_completion([status for status in possible_show], ' ') def command_presence(self, arg): """ @@ -1637,12 +1219,6 @@ class Core(object): if self.current_tab() in tab.privates: self.current_tab().send_chat_state(chatstate, True) - def completion_status(self, the_input): - """ - Completion of /status - """ - return the_input.auto_completion([status for status in possible_show], ' ') - def completion_presence(self, the_input): """ Completion of /presence @@ -1657,96 +1233,6 @@ class Core(object): elif n == 3: return the_input.auto_completion([status for status in possible_show], '') - def command_load(self, arg): - """ - /load - """ - args = arg.split() - if len(args) != 1: - self.command_help('load') - return - filename = args[0] - self.plugin_manager.load(filename) - - def command_unload(self, arg): - """ - /unload - """ - args = arg.split() - if len(args) != 1: - self.command_help('unload') - return - filename = args[0] - self.plugin_manager.unload(filename) - - def command_plugins(self, arg=''): - """ - /plugins - """ - self.information("Plugins currently in use: %s" % repr(list(self.plugin_manager.plugins.keys())), 'Info') - - def command_message(self, arg): - """ - /message [message] - """ - args = common.shell_split(arg) - if len(args) < 1: - self.command_help('message') - return - jid = args[0] - tab = self.open_conversation_window(jid, focus=True) - if len(args) > 1: - tab.command_say(args[1]) - - def command_version(self, arg): - """ - /version - """ - def callback(res): - if not res: - return self.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 _('on an unknown platform')) - self.information(version, 'Info') - - args = common.shell_split(arg) - if len(args) < 1: - return self.command_help('version') - jid = JID(args[0]) - if jid.resource or jid not in roster: - self.xmpp.plugin['xep_0092'].get_version(jid, callback=callback) - elif jid in roster: - for resource in roster[jid].resources: - self.xmpp.plugin['xep_0092'].get_version(resource.jid, callback=callback) - else: - self.xmpp.plugin['xep_0092'].get_version(jid, callback=callback) - - def command_reconnect(self, args=None): - """ - /reconnect - """ - self.disconnect(reconnect=True) - - def command_list(self, arg): - """ - /list - Opens a MucListTab containing the list of the room in the specified server - """ - arg = arg.split() - if len(arg) > 1: - return self.command_help('list') - elif arg: - server = JID(arg[0]).server - else: - if not isinstance(self.current_tab(), tabs.MucTab): - return self.information('Please provide a server', 'Error') - server = JID(self.current_tab().get_name()).server - list_tab = tabs.MucListTab(server) - self.add_tab(list_tab, True) - self.xmpp.plugin['xep_0030'].get_items(jid=server, block=False, callback=list_tab.on_muc_list_item_received) - def command_theme(self, arg=''): """/theme """ args = arg.split() @@ -1812,120 +1298,23 @@ class Core(object): l.remove('') return the_input.auto_completion(l, ' ', quotify=False) - def completion_join(self, the_input): + def command_list(self, arg): """ - Try to complete the server of the MUC's jid (for now only from the currently - open ones) - TODO: have a history of recently joined MUCs, and use that too + /list + Opens a MucListTab containing the list of the room in the specified server """ - txt = the_input.get_text() - if len(txt.split()) != 2: - # we are not on the 1st argument of the command line - return False - jid = JID(txt.split()[1]) - if jid.server: - if jid.resource or jid.full.endswith('/'): - # we are writing the resource: complete the node - if not the_input.last_completion: - try: - response = self.xmpp.plugin['xep_0030'].get_items(jid=jid.server, block=True, timeout=1) - except: - response = None - if response: - items = response['disco_items'].get_items() - else: - return True - items = ['%s/%s' % (tup[0], jid.resource) for tup in items] - for _ in range(len(jid.server) + 2 + len(jid.resource)): - the_input.key_backspace() - else: - items = [] - items.extend(list(self.pending_invites.keys())) - the_input.auto_completion(items, '') - else: - # we are writing the server: complete the server - serv_list = [] - for tab in self.tabs: - if isinstance(tab, tabs.MucTab): - serv_list.append('%s@%s'% (jid.user, JID(tab.get_name()).host)) - serv_list.extend(list(self.pending_invites.keys())) - the_input.auto_completion(serv_list, '') - return True - - def completion_bookmark_local(self, the_input): - """Completion for /bookmark_local""" - txt = the_input.get_text() - args = common.shell_split(txt) - n = len(args) - if txt.endswith(' '): - n += 1 - - if len(args) == 1: - jid = JID('') + arg = arg.split() + if len(arg) > 1: + return self.command_help('list') + elif arg: + server = JID(arg[0]).server else: - jid = JID(args[1]) - if len(args) > 2: - return - if jid.server and (jid.resource or jid.full.endswith('/')): - tab = self.get_tab_by_name(jid.bare, tabs.MucTab) - nicks = [tab.own_nick] if tab else [] - default = os.environ.get('USER') if os.environ.get('USER') else 'poezio' - nick = config.get('default_nick', '') - if not nick: - if not default in nicks: - nicks.append(default) - else: - if not nick in nicks: - nicks.append(nick) - jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks] - return the_input.auto_completion(jids_list, '') - muc_list = [tab.get_name() for tab in self.tabs if isinstance(tab, tabs.MucTab)] - muc_list.append('*') - return the_input.auto_completion(muc_list, '') - - def completion_bookmark(self, the_input): - """Completion for /bookmark""" - txt = the_input.get_text() - args = common.shell_split(txt) - n = len(args) - if txt.endswith(' '): - n += 1 - - if len(args) == 1: - jid = JID('') - else: - jid = JID(args[1]) - - if len(args) == 2: - return the_input.auto_completion(['true', 'false'], '') - if len(args) == 3: - return - - if jid.server and (jid.resource or jid.full.endswith('/')): - tab = self.get_tab_by_name(jid.bare, tabs.MucTab) - nicks = [tab.own_nick] if tab else [] - default = os.environ.get('USER') if os.environ.get('USER') else 'poezio' - nick = config.get('default_nick', '') - if not nick: - if not default in nicks: - nicks.append(default) - else: - if not nick in nicks: - nicks.append(nick) - jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks] - return the_input.auto_completion(jids_list, '') - muc_list = [tab.get_name() for tab in self.tabs if isinstance(tab, tabs.MucTab)] - muc_list.append('*') - return the_input.auto_completion(muc_list, '') - - def completion_version(self, the_input): - """Completion for /version""" - n = len(the_input.get_text().split()) - if n > 2 or (n == 2 and the_input.get_text().endswith(' ')): - return - comp = reduce(lambda x, y: x+y, (jid.resources for jid in roster if len(jid)), []) - comp = (str(res.jid) for res in comp) - return the_input.auto_completion(sorted(comp), '', quotify=False) + if not isinstance(self.current_tab(), tabs.MucTab): + return self.information('Please provide a server', 'Error') + server = JID(self.current_tab().get_name()).server + list_tab = tabs.MucListTab(server) + self.add_tab(list_tab, True) + self.xmpp.plugin['xep_0030'].get_items(jid=server, block=False, callback=list_tab.on_muc_list_item_received) def completion_list(self, the_input): """Completion for /list""" @@ -1937,6 +1326,40 @@ class Core(object): if muc_serv_list: return the_input.auto_completion(muc_serv_list, ' ', quotify=False) + def command_version(self, arg): + """ + /version + """ + def callback(res): + if not res: + return self.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 _('on an unknown platform')) + self.information(version, 'Info') + + args = common.shell_split(arg) + if len(args) < 1: + return self.command_help('version') + jid = JID(args[0]) + if jid.resource or jid not in roster: + self.xmpp.plugin['xep_0092'].get_version(jid, callback=callback) + elif jid in roster: + for resource in roster[jid].resources: + self.xmpp.plugin['xep_0092'].get_version(resource.jid, callback=callback) + else: + self.xmpp.plugin['xep_0092'].get_version(jid, callback=callback) + + def completion_version(self, the_input): + """Completion for /version""" + n = len(the_input.get_text().split()) + if n > 2 or (n == 2 and the_input.get_text().endswith(' ')): + return + comp = reduce(lambda x, y: x+y, (jid.resources for jid in roster if len(jid)), []) + comp = (str(res.jid) for res in comp) + return the_input.auto_completion(sorted(comp), '', quotify=False) + def command_join(self, arg, histo_length=None): """ /join [room][/nick] [password] @@ -2012,15 +1435,45 @@ class Core(object): tab.refresh() self.doupdate() - def get_bookmark_nickname(self, room_name): + def completion_join(self, the_input): """ - Returns the nickname associated with a bookmark - or the default nickname + Try to complete the server of the MUC's jid (for now only from the currently + open ones) + TODO: have a history of recently joined MUCs, and use that too """ - bm = bookmark.get_by_jid(room_name) - if bm: - return bm.nick - return self.own_nick + txt = the_input.get_text() + if len(txt.split()) != 2: + # we are not on the 1st argument of the command line + return False + jid = JID(txt.split()[1]) + if jid.server: + if jid.resource or jid.full.endswith('/'): + # we are writing the resource: complete the node + if not the_input.last_completion: + try: + response = self.xmpp.plugin['xep_0030'].get_items(jid=jid.server, block=True, timeout=1) + except: + response = None + if response: + items = response['disco_items'].get_items() + else: + return True + items = ['%s/%s' % (tup[0], jid.resource) for tup in items] + for _ in range(len(jid.server) + 2 + len(jid.resource)): + the_input.key_backspace() + else: + items = [] + items.extend(list(self.pending_invites.keys())) + the_input.auto_completion(items, '') + else: + # we are writing the server: complete the server + serv_list = [] + for tab in self.tabs: + if isinstance(tab, tabs.MucTab): + serv_list.append('%s@%s'% (jid.user, JID(tab.get_name()).host)) + serv_list.extend(list(self.pending_invites.keys())) + the_input.auto_completion(serv_list, '') + return True def command_bookmark_local(self, arg=''): """ @@ -2073,6 +1526,37 @@ class Core(object): self.information(_('Your local bookmarks are now: %s') % [b for b in bookmark.bookmarks if b.method == 'local'], 'Info') + def completion_bookmark_local(self, the_input): + """Completion for /bookmark_local""" + txt = the_input.get_text() + args = common.shell_split(txt) + n = len(args) + if txt.endswith(' '): + n += 1 + + if len(args) == 1: + jid = JID('') + else: + jid = JID(args[1]) + if len(args) > 2: + return + if jid.server and (jid.resource or jid.full.endswith('/')): + tab = self.get_tab_by_name(jid.bare, tabs.MucTab) + nicks = [tab.own_nick] if tab else [] + default = os.environ.get('USER') if os.environ.get('USER') else 'poezio' + nick = config.get('default_nick', '') + if not nick: + if not default in nicks: + nicks.append(default) + else: + if not nick in nicks: + nicks.append(nick) + jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks] + return the_input.auto_completion(jids_list, '') + muc_list = [tab.get_name() for tab in self.tabs if isinstance(tab, tabs.MucTab)] + muc_list.append('*') + return the_input.auto_completion(muc_list, '') + def command_bookmark(self, arg=''): """ /bookmark [room][/nick] [autojoin] [password] @@ -2145,6 +1629,41 @@ class Core(object): self.information(_('Your remote bookmarks are now: %s') % [b for b in bookmark.bookmarks if b.method in ('pep', 'privatexml')], 'Info') + def completion_bookmark(self, the_input): + """Completion for /bookmark""" + txt = the_input.get_text() + args = common.shell_split(txt) + n = len(args) + if txt.endswith(' '): + n += 1 + + if len(args) == 1: + jid = JID('') + else: + jid = JID(args[1]) + + if len(args) == 2: + return the_input.auto_completion(['true', 'false'], '') + if len(args) == 3: + return + + if jid.server and (jid.resource or jid.full.endswith('/')): + tab = self.get_tab_by_name(jid.bare, tabs.MucTab) + nicks = [tab.own_nick] if tab else [] + default = os.environ.get('USER') if os.environ.get('USER') else 'poezio' + nick = config.get('default_nick', '') + if not nick: + if not default in nicks: + nicks.append(default) + else: + if not nick in nicks: + nicks.append(nick) + jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks] + return the_input.auto_completion(jids_list, '') + muc_list = [tab.get_name() for tab in self.tabs if isinstance(tab, tabs.MucTab)] + muc_list.append('*') + return the_input.auto_completion(muc_list, '') + def command_bookmarks(self, arg=''): """/bookmarks""" self.information(_('Your remote bookmarks are: %s') % @@ -2255,44 +1774,6 @@ class Core(object): end_list = [config.get(args[2], '', args[1]), ''] return the_input.auto_completion(end_list, '') - def completion_server_cycle(self, the_input): - """Completion for /server_cycle""" - txt = the_input.get_text() - args = txt.split() - n = len(args) - if txt.endswith(' '): - n += 1 - if n == 2: - serv_list = [] - for tab in self.tabs: - if isinstance(tab, tabs.MucTab): - serv = JID(tab.get_name()).server - if not serv in serv_list: - serv_list.append(serv) - return the_input.auto_completion(serv_list, ' ') - - def close_tab(self, tab=None): - """ - Close the given tab. If None, close the current one - """ - tab = tab or self.current_tab() - if isinstance(tab, tabs.RosterInfoTab): - return # The tab 0 should NEVER be closed - del tab.key_func # Remove self references - del tab.commands # and make the object collectable - tab.on_close() - self.tabs.remove(tab) - if tab.get_name() in logger.fds: - logger.fds[tab.get_name()].close() - log.debug("Log file for %s closed.", tab.get_name()) - del logger.fds[tab.get_name()] - self.tabs[0].on_gain_focus() - self.refresh_window() - import gc - gc.collect() - log.debug('___ Referrers of closing tab:\n%s\n______', gc.get_referrers(tab)) - del tab - def command_server_cycle(self, arg=''): """ Do a /cycle on each room of the given server. If none, do it on the current tab @@ -2317,6 +1798,96 @@ class Core(object): tab.joined = False self.command_join('"%s/%s"' %(tab.get_name(), tab.own_nick)) + def completion_server_cycle(self, the_input): + """Completion for /server_cycle""" + txt = the_input.get_text() + args = txt.split() + n = len(args) + if txt.endswith(' '): + n += 1 + if n == 2: + serv_list = [] + for tab in self.tabs: + if isinstance(tab, tabs.MucTab): + serv = JID(tab.get_name()).server + if not serv in serv_list: + serv_list.append(serv) + return the_input.auto_completion(serv_list, ' ') + + def command_invite(self, arg): + """/invite [reason]""" + args = common.shell_split(arg) + if len(args) < 2: + return + reason = args[2] if len(args) > 2 else '' + to = args[0] + room = args[1] + self.xmpp.plugin['xep_0045'].invite(room, to, reason) + + def completion_invite(self, the_input): + """Completion for /invite""" + txt = the_input.get_text() + args = common.shell_split(txt) + n = len(args) + if txt.endswith(' '): + n += 1 + if n == 2: + return the_input.auto_completion([jid for jid in roster.jids()], '') + elif n == 3: + rooms = [] + for tab in self.tabs: + if isinstance(tab, tabs.MucTab) and tab.joined: + rooms.append(tab.get_name()) + return the_input.auto_completion(rooms, '') + + def command_decline(self, arg): + """/decline [reason]""" + args = common.shell_split(arg) + if not len(args): + return + jid = JID(args[0]) + if jid.bare not in self.pending_invites: + return + reason = args[1] if len(args) > 1 else '' + del self.pending_invites[jid.bare] + self.xmpp.plugin['xep_0045'].decline_invite(jid.bare, self.pending_invites[jid.bare], reason) + + def completion_decline(self, the_input): + """Completion for /decline""" + txt = the_input.get_text() + args = common.shell_split(txt) + n = len(args) + if txt.endswith(' '): + n += 1 + if n == 2: + return the_input.auto_completion(list(self.pending_invites.keys()), '') + + ### Commands without a completion in this class ### + + def command_invitations(self, arg=''): + """/invitations""" + build = "" + for invite in self.pending_invites: + build += "%s by %s" % (invite, JID(self.pending_invites[invite]).bare) + if self.pending_invites: + build = "You are invited to the following rooms:\n" + build + else: + build = "You are do not have any pending invitation." + self.information(build, 'Info') + + def command_quit(self, arg=''): + """ + /quit + """ + if len(arg.strip()) != 0: + msg = arg + else: + msg = None + self.disconnect(msg) + self.running = False + self.reset_curses() + sys.exit() + def command_bind(self, arg): """ Bind a key. @@ -2343,205 +1914,684 @@ class Core(object): self.add_tab(new_tab, True) self.refresh_window() - def go_to_room_number(self): + def command_rawxml(self, arg): """ - Read 2 more chars and go to the tab - with the given number + /rawxml """ - char = self.read_keyboard()[0] + if not arg: + return + try: - nb1 = int(char) - except ValueError: - return - char = self.read_keyboard()[0] - try: - nb2 = int(char) - except ValueError: - return - self.command_win('%s%s' % (nb1, nb2)) - - def information(self, msg, typ=''): - """ - Displays an informational message in the "Info" buffer - """ - nb_lines = self.information_buffer.add_message(msg, nickname=typ) - if isinstance(self.current_tab(), tabs.RosterInfoTab): - self.refresh_window() - elif typ != '' and typ.lower() in config.get('information_buffer_popup_on', - 'error roster warning help info').split(): - popup_time = config.get('popup_time', 4) + (nb_lines - 1) * 2 - self.pop_information_win_up(nb_lines, popup_time) - else: - if self.information_win_size != 0: - self.information_win.refresh() - self.current_tab().input.refresh() - - def disconnect(self, msg='', reconnect=False): - """ - Disconnect from remote server and correctly set the states of all - parts of the client (for example, set the MucTabs as not joined, etc) - """ - msg = msg or '' - for tab in self.tabs: - if isinstance(tab, tabs.MucTab) and tab.joined: - tab.command_part(msg) - self.save_config() - # Ugly fix thanks to gmail servers - self.xmpp.disconnect(reconnect) - - def command_quit(self, arg=''): - """ - /quit - """ - if len(arg.strip()) != 0: - msg = arg - else: - msg = None - self.disconnect(msg) - self.running = False - self.reset_curses() - sys.exit() - - def save_config(self): - """ - Save config in the file just before exit - """ - roster.save_to_config_file() - config.set_and_save('info_win_height', self.information_win_size, 'var') - - def do_command(self, key, raw): - if not key: - return - return self.current_tab().on_input(key, raw) - - def on_roster_enter_key(self, roster_row): - """ - when enter is pressed on the roster window - """ - if isinstance(roster_row, Contact): - if not self.get_conversation_by_jid(roster_row.bare_jid): - self.open_conversation_window(roster_row.bare_jid) - else: - self.focus_tab_named(roster_row.bare_jid) - if isinstance(roster_row, Resource): - if not self.get_conversation_by_jid(roster_row.jid.full): - self.open_conversation_window(roster_row.jid.full) - else: - self.focus_tab_named(roster_row.jid.full) - self.refresh_window() - - def remove_timed_event(self, event): - """Remove an existing timed event""" - if event and event in self.timed_events: - self.timed_events.remove(event) - - def add_timed_event(self, event): - """Add a new timed event""" - self.timed_events.add(event) - - def check_timed_events(self): - """Check for the execution of timed events""" - now = datetime.now() - for event in self.timed_events: - if event.has_timed_out(now): - res = event() - if not res: - self.timed_events.remove(event) - break - - def execute(self,line): - """ - Execute the /command or just send the line on the current room - """ - if line == "": - return - if line.startswith('/'): - command = line.strip()[:].split()[0][1:] - arg = line[2+len(command):] # jump the '/' and the ' ' - # example. on "/link 0 open", command = "link" and arg = "0 open" - if command in self.commands: - func = self.commands[command][0] - func(arg) - return - else: - self.information(_("Unknown command (%s)") % (command), _('Error')) - - def doupdate(self): - if not self.running or self.background is True: - return - curses.doupdate() - - def send_message(self, msg): - """ - Function to use in plugins to send a message in the current conversation. - Returns False if the current tab is not a conversation tab - """ - if not isinstance(self.current_tab(), tabs.ChatTab): - return False - self.current_tab().command_say(msg) - return True - - def exec_command(self, command): - """ - Execute an external command on the local or a remote - machine, depending on the conf. For example, to open a link in a - browser, do exec_command("firefox http://poezio.eu"), - and this will call the command on the correct computer. - The remote execution is done by writing the command on a fifo. - That fifo has to be on the machine where poezio is running, and - accessible (through sshfs for example) from the local machine (where - poezio is not running). A very simple daemon reads on that fifo, - and executes any command that is read in it. - """ - command = '%s\n' % (command,) - if config.get('exec_remote', 'false') == 'true': - # We just write the command in the fifo - if not self.remote_fifo: - try: - self.remote_fifo = Fifo(os.path.join(config.get('remote_fifo_path', './'), 'poezio.fifo'), 'w') - except (OSError, IOError) as e: - self.information('Could not open fifo file for writing: %s' % (e,), 'Error') - return - try: - self.remote_fifo.write(command) - except (IOError) as e: - self.information('Could not execute [%s]: %s' % (command.strip(), e,), 'Error') - self.remote_fifo = None - else: - e = Executor(command.strip()) - try: - e.start() - except ValueError as e: # whenever shlex fails - self.information('%s' % (e,), 'Error') - - def get_conversation_messages(self): - """ - Returns a list of all the messages in the current chat. - If the current tab is not a ChatTab, returns None. - - Messages are namedtuples of the form - ('txt nick_color time str_time nickname user') - """ - if not isinstance(self.current_tab(), tabs.ChatTab): - return None - return self.current_tab().get_conversation_messages() - - def insert_input_text(self, text): - """ - Insert the given text into the current input - """ - self.do_command(text, True) - - def try_execute(self, line): - """ - Try to execute a command in the current tab - """ - line = '/' + line - try: - self.current_tab().execute_command(line) + StanzaBase(self.xmpp, xml=ET.fromstring(arg)).send() except: import traceback - log.debug('Execute failed:\n%s', traceback.format_exc()) + self.information(_('Could not send custom stanza'), 'Error') + log.debug(_("Could not send custom stanza:\n") + traceback.format_exc()) + + def command_load(self, arg): + """ + /load + """ + args = arg.split() + if len(args) != 1: + self.command_help('load') + return + filename = args[0] + self.plugin_manager.load(filename) + + def command_unload(self, arg): + """ + /unload + """ + args = arg.split() + if len(args) != 1: + self.command_help('unload') + return + filename = args[0] + self.plugin_manager.unload(filename) + + def command_plugins(self, arg=''): + """ + /plugins + """ + self.information("Plugins currently in use: %s" % repr(list(self.plugin_manager.plugins.keys())), 'Info') + + def command_message(self, arg): + """ + /message [message] + """ + args = common.shell_split(arg) + if len(args) < 1: + self.command_help('message') + return + jid = args[0] + tab = self.open_conversation_window(jid, focus=True) + if len(args) > 1: + tab.command_say(args[1]) + + def command_reconnect(self, args=None): + """ + /reconnect + """ + self.disconnect(reconnect=True) + + def command_xml_tab(self, arg=''): + """/xml_tab""" + self.xml_tabs += 1 + tab = tabs.XMLTab() + self.add_tab(tab, True) + + + +####################### XMPP Event Handlers ################################## + + ### Invites ### + + def on_groupchat_invite(self, message): + jid = message['from'] + if jid.bare in self.pending_invites: + return + # there are 2 'x' tags in the messages, making message['x'] useless + invite = StanzaBase(self.xmpp, xml=message.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite')) + inviter = invite['from'] + reason = invite['reason'] + password = invite['password'] + msg = "You are invited to the room %s by %s" % (jid.full, inviter.full) + if reason: + msg += "because: %s" % reason + if password: + msg += ". The password is \"%s\"." % password + self.information(msg, 'Info') + if 'invite' in config.get('beep_on', 'invite').split(): + curses.beep() + self.pending_invites[jid.bare] = inviter.full + + def on_groupchat_decline(self, decline): + pass + + ### "classic" messages ### + + def on_message(self, message): + """ + When receiving private message from a muc OR a normal message + (from one of our contacts) + """ + if message.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite') != None: + return + if message['type'] == 'groupchat': + return + # Differentiate both type of messages, and call the appropriate handler. + jid_from = message['from'] + for tab in self.tabs: + if tab.get_name() == jid_from.bare and isinstance(tab, tabs.MucTab): + if message['type'] == 'error': + return self.room_error(message, jid_from) + else: + return self.on_groupchat_private_message(message) + return self.on_normal_message(message) + + def on_normal_message(self, message): + """ + When receiving "normal" messages (from someone in our roster) + """ + jid = message['from'] + body = xhtml.get_body_from_message_stanza(message) + if message['type'] == 'error': + return self.information(self.get_error_message(message, deprecated=True), 'Error') + if not body: + return + conversation = self.get_tab_of_conversation_with_jid(jid, create=True) + self.events.trigger('conversation_msg', message, conversation) + body = xhtml.get_body_from_message_stanza(message) + if jid.bare in roster: + remote_nick = roster[jid.bare].name or jid.user + else: + remote_nick = jid.user + delay_tag = message.find('{urn:xmpp:delay}delay') + if delay_tag is not None: + delayed = True + date = common.datetime_tuple(delay_tag.attrib['stamp']) + else: + delayed = False + date = None + conversation._text_buffer.add_message(body, date, nickname=remote_nick, nick_color=get_theme().COLOR_REMOTE_USER, history=delayed) + if conversation.remote_wants_chatstates is None and not delayed: + if message['chat_state']: + conversation.remote_wants_chatstates = True + else: + conversation.remote_wants_chatstates = False + logger.log_message(jid.bare, remote_nick, body) + if 'private' in config.get('beep_on', 'highlight private').split(): + if config.get_by_tabname('disable_beep', 'false', jid.bare, False).lower() != 'true': + curses.beep() + if self.current_tab() is not conversation: + conversation.state = 'private' + self.refresh_tab_win() + else: + self.refresh_window() + + def on_groupchat_message(self, message): + """ + Triggered whenever a message is received from a multi-user chat room. + """ + if message['subject']: + return + delay_tag = message.find('{urn:xmpp:delay}delay') + if delay_tag is not None: + delayed = True + date = common.datetime_tuple(delay_tag.attrib['stamp']) + else: + # We support the OLD and deprecated XEP: http://xmpp.org/extensions/xep-0091.html + # But it sucks, please, Jabber servers, don't do this :( + delay_tag = message.find('{jabber:x:delay}x') + if delay_tag is not None: + delayed = True + date = common.datetime_tuple(delay_tag.attrib['stamp']) + else: + delayed = False + date = None + nick_from = message['mucnick'] + room_from = message.getMucroom() + if message['type'] == 'error': # Check if it's an error + return self.room_error(message, room_from) + tab = self.get_tab_by_name(room_from, tabs.MucTab) + old_state = tab.state + if not tab: + self.information(_("message received for a non-existing room: %s") % (room_from)) + return + if tab.get_user_by_name(nick_from) and\ + tab.get_user_by_name(nick_from) in tab.ignores: + return + self.events.trigger('muc_msg', message, tab) + body = xhtml.get_body_from_message_stanza(message) + if body: + date = date if delayed == True else None + if tab.add_message(body, date, nick_from, history=True if date else False): + self.events.trigger('highlight', message, tab) + if tab is self.current_tab(): + tab.text_win.refresh() + tab.info_header.refresh(tab, tab.text_win) + tab.input.refresh() + self.doupdate() + elif tab.state != old_state: + self.refresh_tab_win() + self.current_tab().input.refresh() + self.doupdate() + if 'message' in config.get('beep_on', 'highlight private').split(): + if config.get_by_tabname('disable_beep', 'false', room_from, False).lower() != 'true': + curses.beep() + + def on_groupchat_private_message(self, message): + """ + We received a Private Message (from someone in a Muc) + """ + jid = message['from'] + nick_from = jid.resource + room_from = jid.bare + body = xhtml.get_body_from_message_stanza(message) + tab = self.get_tab_by_name(jid.full, tabs.PrivateTab) # get the tab with the private conversation + ignore = config.get_by_tabname('ignore_private', 'false', + room_from).lower() == 'true' + if not tab: # It's the first message we receive: create the tab + if body and not ignore: + tab = self.open_private_window(room_from, nick_from, False) + if ignore: + self.events.trigger('ignored_private', message, tab) + msg = config.get_by_tabname('private_auto_response', None, room_from) + if msg and body: + self.xmpp.send_message(mto=jid.full, mbody=msg, mtype='chat') + return + self.events.trigger('private_msg', message, tab) + if not body or not tab: + return + tab.add_message(body, time=None, nickname=nick_from, + forced_user=self.get_tab_by_name(room_from, tabs.MucTab).get_user_by_name(nick_from)) + conversation = self.get_tab_by_name(jid.full, tabs.PrivateTab) + if conversation and conversation.remote_wants_chatstates is None: + if message['chat_state']: + conversation.remote_wants_chatstates = True + else: + conversation.remote_wants_chatstates = False + if 'private' in config.get('beep_on', 'highlight private').split(): + if config.get_by_tabname('disable_beep', 'false', jid.full, False).lower() != 'true': + curses.beep() + logger.log_message(jid.full.replace('/', '\\'), nick_from, body) + if conversation is self.current_tab(): + self.refresh_window() + else: + conversation.state = 'private' + self.refresh_tab_win() + + ### Chatstates ### + + def on_chatstate_active(self, message): + self.on_chatstate(message, "active") + + def on_chatstate_inactive(self, message): + self.on_chatstate(message, "inactive") + + def on_chatstate_composing(self, message): + self.on_chatstate(message, "composing") + + def on_chatstate_paused(self, message): + self.on_chatstate(message, "paused") + + def on_chatstate_gone(self, message): + self.on_chatstate(message, "gone") + + def on_chatstate(self, message, state): + if message['type'] == 'chat': + if not self.on_chatstate_normal_conversation(message, state): + tab = self.get_tab_by_name(message['from'].full, tabs.PrivateTab) + if not tab: + return + self.on_chatstate_private_conversation(message, state) + elif message['type'] == 'groupchat': + self.on_chatstate_groupchat_conversation(message, state) + + def on_chatstate_normal_conversation(self, message, state): + tab = self.get_tab_of_conversation_with_jid(message['from'], False) + if not tab: + return False + self.events.trigger('normal_chatstate', message, tab) + tab.chatstate = state + if tab == self.current_tab(): + tab.refresh_info_header() + self.doupdate() + return True + + def on_chatstate_private_conversation(self, message, state): + """ + Chatstate received in a private conversation from a MUC + """ + tab = self.get_tab_by_name(message['from'].full, tabs.PrivateTab) + if not tab: + return + self.events.trigger('private_chatstate', message, tab) + tab.chatstate = state + if tab == self.current_tab(): + tab.refresh_info_header() + self.doupdate() + return True + + def on_chatstate_groupchat_conversation(self, message, state): + """ + Chatstate received in a MUC + """ + nick = message['mucnick'] + room_from = message.getMucroom() + tab = self.get_tab_by_name(room_from, tabs.MucTab) + if tab and tab.get_user_by_name(nick): + self.events.trigger('muc_chatstate', message, tab) + tab.get_user_by_name(nick).chatstate = state + if tab == self.current_tab(): + tab.user_win.refresh(tab.users) + tab.input.refresh() + self.doupdate() + + ### subscription-related handlers ### + + def on_roster_update(self, iq): + """ + The roster was received. + """ + for item in iq['roster']: + jid = item['jid'] + if item['subscription'] == 'remove': + del roster[jid] + else: + roster.update_contact_groups(jid) + if isinstance(self.current_tab(), tabs.RosterInfoTab): + self.refresh_window() + + def on_subscription_request(self, presence): + """subscribe received""" + jid = presence['from'].bare + contact = roster[jid] + if contact.subscription in ('from', 'both'): + return + elif contact.subscription == 'to': + self.xmpp.sendPresence(pto=jid, ptype='subscribed') + self.xmpp.sendPresence(pto=jid) + else: + roster.update_contact_groups(contact) + contact.pending_in = True + self.information('%s wants to subscribe to your presence' % jid, 'Roster') + self.get_tab_by_number(0).state = 'highlight' + if isinstance(self.current_tab(), tabs.RosterInfoTab): + self.refresh_window() + + def on_subscription_authorized(self, presence): + """subscribed received""" + jid = presence['from'].bare + contact = roster[jid] + if contact.subscription not in ('both', 'from'): + self.information('%s accepted your contact proposal' % jid, 'Roster') + if contact.pending_out: + contact.pending_out = False + if isinstance(self.current_tab(), tabs.RosterInfoTab): + self.refresh_window() + + def on_subscription_remove(self, presence): + """unsubscribe received""" + jid = presence['from'].bare + contact = roster[jid] + if not contact: + return + self.information('%s does not want to receive your status anymore.' % jid, 'Roster') + self.get_tab_by_number(0).state = 'highlight' + if isinstance(self.current_tab(), tabs.RosterInfoTab): + self.refresh_window() + + def on_subscription_removed(self, presence): + """unsubscribed received""" + jid = presence['from'].bare + contact = roster[jid] + if not contact: + return + if contact.pending_out: + self.information('%s rejected your contact proposal' % jid, 'Roster') + contact.pending_out = False + else: + self.information('%s does not want you to receive his/her/its status anymore.'%jid, 'Roster') + self.get_tab_by_number(0).state = 'highlight' + if isinstance(self.current_tab(), tabs.RosterInfoTab): + self.refresh_window() + + ### Presence-related handlers ### + + def on_presence(self, presence): + jid = presence['from'] + contact = roster[jid.bare] + if contact is None: + return + self.events.trigger('normal_presence', presence, contact[jid.full]) + tab = self.get_tab_of_conversation_with_jid(jid, create=False) + if isinstance(self.current_tab(), tabs.RosterInfoTab): + self.refresh_window() + elif self.current_tab() == tab: + tab.refresh() + self.doupdate() + + def on_got_offline(self, presence): + """ + A JID got offline + """ + jid = presence['from'] + logger.log_roster_change(jid.bare, 'got offline') + # If a resource got offline, display the message in the conversation with this + # precise resource. + if jid.resource: + self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x191}offline' % (jid.full)) + if jid.server in roster.blacklist: + return + self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x191}offline' % (jid.bare)) + self.information('\x193}%s \x195}is \x191}offline' % (jid.bare), 'Roster') + if isinstance(self.current_tab(), tabs.RosterInfoTab): + self.refresh_window() + + def on_got_online(self, presence): + jid = presence['from'] + contact = roster[jid.bare] + if contact is None: + # Todo, handle presence coming from contacts not in roster + return + logger.log_roster_change(jid.bare, 'got online') + resource = Resource(jid.full, { + 'priority': presence.get_priority() or 0, + 'status': presence['status'], + 'show': presence['show'], + }) + self.events.trigger('normal_presence', presence, resource) + self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x194}online' % (jid.full)) + if time.time() - self.connection_time > 20: + # We do not display messages if we recently logged in + if presence['status']: + self.information("\x193}%s \x195}is \x194}online\x195} (\x19o%s\x195})" % (resource.jid.bare, presence['status']), "Roster") + else: + self.information("\x193}%s \x195}is \x194}online\x195}" % resource.jid.bare, "Roster") + self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x194}online' % (jid.bare)) + if isinstance(self.current_tab(), tabs.RosterInfoTab): + self.refresh_window() + + def on_groupchat_presence(self, presence): + """ + Triggered whenever a presence stanza is received from a user in a multi-user chat room. + Display the presence on the room window and update the + presence information of the concerned user + """ + from_room = presence['from'].bare + tab = self.get_tab_by_name(from_room, tabs.MucTab) + if tab: + self.events.trigger('muc_presence', presence, tab) + tab.handle_presence(presence) + + + ### Connection-related handlers ### + + def on_failed_connection(self): + """ + We cannot contact the remote server + """ + self.information(_("Connection to remote server failed")) + + def on_disconnected(self, event): + """ + When we are disconnected from remote server + """ + for tab in self.tabs: + if isinstance(tab, tabs.MucTab): + tab.disconnect() + self.information(_("Disconnected from server.")) + + def on_failed_auth(self, event): + """ + Authentication failed + """ + self.information(_("Authentication failed.")) + + def on_connected(self, event): + """ + Remote host responded, but we are not yet authenticated + """ + self.information(_("Connected to server.")) + + def on_session_start(self, event): + """ + Called when we are connected and authenticated + """ + self.connection_time = time.time() + if not self.plugins_autoloaded: # Do not reload plugins on reconnection + self.autoload_plugins() + self.information(_("Authentication success.")) + self.information(_("Your JID is %s") % self.xmpp.boundjid.full) + if not self.xmpp.anon: + # request the roster + self.xmpp.get_roster() + # send initial presence + if config.get('send_initial_presence', 'true').lower() == 'true': + pres = self.xmpp.make_presence() + self.events.trigger('send_normal_presence', pres) + pres.send() + bookmark.get_local() + if not self.xmpp.anon and not config.get('use_remote_bookmarks', 'true').lower() == 'false': + bookmark.get_remote(self.xmpp) + for bm in [item for item in bookmark.bookmarks if item.autojoin]: + tab = self.get_tab_by_name(bm.jid, tabs.MucTab) + if not tab: + self.open_new_room(bm.jid, bm.nick, False) + nick = bm.nick if bm.nick else self.own_nick + self.initial_joins.append(bm.jid) + muc.join_groupchat(self.xmpp, bm.jid, nick) + + ### Other handlers ### + + def on_status_codes(self, message): + """ + Handle groupchat messages with status codes. + Those are received when a room configuration change occurs. + """ + room_from = message['from'] + tab = self.get_tab_by_name(room_from, tabs.MucTab) + status_codes = set([s.attrib['code'] for s in message.findall('{%s}x/{%s}status' % (tabs.NS_MUC_USER, tabs.NS_MUC_USER))]) + if '101' in status_codes: + self.information('Your affiliation in the room %s changed' % room_from, 'Info') + elif tab and status_codes: + show_unavailable = '102' in status_codes + hide_unavailable = '103' in status_codes + non_priv = '104' in status_codes + logging_on = '170' in status_codes + logging_off= '171' in status_codes + non_anon = '172' in status_codes + semi_anon = '173' in status_codes + full_anon = '174' in status_codes + modif = False + if show_unavailable or hide_unavailable or non_priv or logging_off\ + or non_anon or semi_anon or full_anon: + tab.add_message('\x19%(info_col)s}Info: A configuration change not privacy-related occured.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + modif = True + if show_unavailable: + tab.add_message('\x19%(info_col)s}Info: The unavailable members are now shown.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + elif hide_unavailable: + tab.add_message('\x19%(info_col)s}Info: The unavailable members are now hidden.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + if non_anon: + tab.add_message('\x191}Warning:\x19%(info_col)s} The room is now not anonymous. (public JID)' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + elif semi_anon: + tab.add_message('\x19%(info_col)s}Info: The room is now semi-anonymous. (moderators-only JID)' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + elif full_anon: + tab.add_message('\x19%(info_col)s}Info: The room is now fully anonymous.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + if logging_on: + tab.add_message('\x191}Warning: \x19%(info_col)s}This room is publicly logged' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + elif logging_off: + tab.add_message('\x19%(info_col)s}Info: This room is not logged anymore.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}) + if modif: + self.refresh_window() + + def on_groupchat_subject(self, message): + """ + Triggered when the topic is changed. + """ + nick_from = message['mucnick'] + room_from = message.getMucroom() + tab = self.get_tab_by_name(room_from, tabs.MucTab) + subject = message['subject'] + if not subject or not tab: + return + if nick_from: + self.add_message_to_text_buffer(tab._text_buffer, + _("\x19%(info_col)s}%(nick)s set the subject to: %(subject)s") % + {'info_col': get_theme().COLOR_INFORMATION_TEXT[0], 'nick':nick_from, 'subject':subject}, + time=None) + else: + self.add_message_to_text_buffer(tab._text_buffer, _("\x19%(info_col)s}The subject is: %(subject)s") % + {'subject':subject, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]}, + time=None) + tab.topic = subject + if self.get_tab_by_name(room_from, tabs.MucTab) is self.current_tab(): + self.refresh_window() + + def on_data_form(self, message): + """ + When a data form is received + """ + self.information('%s' % message) + + def on_attention(self, message): + """ + Attention probe received. + """ + jid_from = message['from'] + self.information('%s requests your attention!' % jid_from, 'Info') + for tab in self.tabs: + if tab.get_name() == jid_from: + tab.state = 'attention' + self.refresh_tab_win() + return + for tab in self.tabs: + if tab.get_name() == jid_from.bare: + tab.state = 'attention' + self.refresh_tab_win() + return + self.information('%s tab not found.' % jid_from, 'Error') + + def room_error(self, error, room_name): + """ + Display the error in the tab + """ + tab = self.get_tab_by_name(room_name) + error_message = self.get_error_message(error) + self.add_message_to_text_buffer(tab._text_buffer, error_message) + code = error['error']['code'] + if code == '401': + msg = _('To provide a password in order to join the room, type "/join / password" (replace "password" by the real password)') + self.add_message_to_text_buffer(tab._text_buffer, msg) + if code == '409': + if config.get('alternative_nickname', '') != '': + self.command_join('%s/%s'% (tab.name, tab.own_nick+config.get('alternative_nickname', ''))) + else: + self.add_message_to_text_buffer(tab._text_buffer, _('You can join the room with an other nick, by typing "/join /other_nick"')) + self.refresh_window() + + def outgoing_stanza(self, stanza): + """ + We are sending a new stanza, write it in the xml buffer if needed. + """ + if self.xml_tabs: + self.add_message_to_text_buffer(self.xml_buffer, '\x191}<--\x19o %s' % stanza) + if isinstance(self.current_tab(), tabs.XMLTab): + self.current_tab().refresh() + self.doupdate() + + def incoming_stanza(self, stanza): + """ + We are receiving a new stanza, write it in the xml buffer if needed. + """ + if self.xml_tabs: + self.add_message_to_text_buffer(self.xml_buffer, '\x192}-->\x19o %s' % stanza) + if isinstance(self.current_tab(), tabs.XMLTab): + self.current_tab().refresh() + self.doupdate() + + def validate_ssl(self, pem): + """ + Check the server certificate using the sleekxmpp ssl_cert event + """ + if config.get('ignore_certificate', 'false').lower() == 'true': + return + cert = config.get('certificate', '') + der = ssl.PEM_cert_to_DER_cert(pem) + found_cert = sha1(der).hexdigest() + if cert: + if found_cert == cert: + log.debug('Cert %s OK', found_cert) + return + else: + saved_input = self.current_tab().input + log.debug('\nWARNING: CERTIFICATE CHANGED old: %s, new: %s\n', cert, found_cert) + input = windows.YesNoInput(text="WARNING! Certificate hash changed to %s. Accept? (y/n)" % found_cert) + self.current_tab().input = input + input.resize(1, self.current_tab().width, self.current_tab().height-1, 0) + input.refresh() + self.doupdate() + self.paused = True + while input.value is None: + pass + self.current_tab().input = saved_input + self.paused = False + if input.value: + self.information('Setting new certificate: old: %s, new: %s' % (cert, found_cert), 'Info') + log.debug('Setting certificate to %s', found_cert) + config.set_and_save('certificate', found_cert) + else: + self.information('You refused to validate the certificate. You are now disconnected', 'Info') + self.xmpp.disconnect() + else: + log.debug('First time. Setting certificate to %s', found_cert) + config.set_and_save('certificate', found_cert) + + + + class KeyDict(dict): """