diff options
Diffstat (limited to 'src/core')
-rw-r--r-- | src/core/commands.py | 78 | ||||
-rw-r--r-- | src/core/completions.py | 17 | ||||
-rw-r--r-- | src/core/core.py | 322 | ||||
-rw-r--r-- | src/core/handlers.py | 195 |
4 files changed, 349 insertions, 263 deletions
diff --git a/src/core/commands.py b/src/core/commands.py index d212de9b..4a8f7f19 100644 --- a/src/core/commands.py +++ b/src/core/commands.py @@ -6,15 +6,16 @@ import logging log = logging.getLogger(__name__) +import functools import os import sys from datetime import datetime from gettext import gettext as _ from xml.etree import cElementTree as ET -from sleekxmpp.xmlstream.stanzabase import StanzaBase -from sleekxmpp.xmlstream.handler import Callback -from sleekxmpp.xmlstream.matcher import StanzaPath +from slixmpp.xmlstream.stanzabase import StanzaBase +from slixmpp.xmlstream.handler import Callback +from slixmpp.xmlstream.matcher import StanzaPath import bookmark import common @@ -276,7 +277,6 @@ def command_list(self, arg): self.add_tab(list_tab, True) cb = list_tab.on_muc_list_item_received self.xmpp.plugin['xep_0030'].get_items(jid=server, - block=False, callback=cb) def command_version(self, arg): @@ -439,7 +439,7 @@ def command_bookmark_local(self, arg=''): new_bookmarks.extend(bookmark.bookmarks) bookmark.bookmarks = new_bookmarks bookmark.save_local() - bookmark.save_remote(self.xmpp) + bookmark.save_remote(self.xmpp, None) self.information('Bookmarks added and saved.', 'Info') return else: @@ -507,12 +507,13 @@ def command_bookmark(self, arg=''): new_bookmarks.append(b) new_bookmarks.extend(bookmark.bookmarks) bookmark.bookmarks = new_bookmarks - - if bookmark.save_remote(self.xmpp): - bookmark.save_local() - self.information("Bookmarks added.", "Info") - else: - self.information("Could not add the bookmarks.", "Info") + def _cb(self, iq): + if iq["type"] != "error": + bookmark.save_local() + self.information("Bookmarks added.", "Info") + else: + self.information("Could not add the bookmarks.", "Info") + bookmark.save_remote(self.xmpp, functools.partial(_cb, self)) return else: info = safeJID(args[0]) @@ -541,14 +542,16 @@ def command_bookmark(self, arg=''): if password: bm.password = password bm.autojoin = autojoin - if bookmark.save_remote(self.xmpp): - self.information('Bookmark added.', 'Info') + def _cb(self, iq): + if iq["type"] != "error": + self.information('Bookmark added.', 'Info') + else: + self.information("Could not add the bookmarks.", "Info") + bookmark.save_remote(self.xmpp, functools.partial(_cb, self)) remote = [] for each in bookmark.bookmarks: if each.method in ('pep', 'privatexml'): remote.append(each) - self.information(_('Your remote bookmarks are now: %s') % remote, - _('Info')) def command_bookmarks(self, arg=''): """/bookmarks""" @@ -718,7 +721,6 @@ def command_last_activity(self, arg): if jid == '': return self.command_help('last_activity') self.xmpp.plugin['xep_0012'].get_last_activity(jid, - block=False, callback=callback) def command_mood(self, arg): @@ -727,7 +729,8 @@ def command_mood(self, arg): """ args = common.shell_split(arg) if not args: - return self.xmpp.plugin['xep_0107'].stop(block=False) + self.xmpp.plugin['xep_0107'].stop() + return mood = args[0] if mood not in pep.MOODS: return self.information(_('%s is not a correct value for a mood.') @@ -737,10 +740,8 @@ def command_mood(self, arg): text = args[1] else: text = None - self.xmpp.plugin['xep_0107'].publish_mood(mood, - text, - callback=dumb_callback, - block=False) + self.xmpp.plugin['xep_0107'].publish_mood(mood, text, + callback=dumb_callback) def command_activity(self, arg): """ @@ -749,7 +750,8 @@ def command_activity(self, arg): args = common.shell_split(arg) length = len(args) if not length: - return self.xmpp.plugin['xep_0108'].stop(block=False) + self.xmpp.plugin['xep_0108'].stop() + return general = args[0] if general not in pep.ACTIVITIES: return self.information(_('%s is not a correct value for an activity') @@ -769,11 +771,8 @@ def command_activity(self, arg): return self.information(_('%s is not a correct value ' 'for an activity') % specific, _('Error')) - self.xmpp.plugin['xep_0108'].publish_activity(general, - specific, - text, - callback=dumb_callback, - block=False) + self.xmpp.plugin['xep_0108'].publish_activity(general, specific, text, + callback=dumb_callback) def command_gaming(self, arg): """ @@ -781,7 +780,8 @@ def command_gaming(self, arg): """ args = common.shell_split(arg) if not args: - return self.xmpp.plugin['xep_0196'].stop(block=False) + self.xmpp.plugin['xep_0196'].stop() + return name = args[0] if len(args) > 1: address = args[1] @@ -789,8 +789,7 @@ def command_gaming(self, arg): address = None return self.xmpp.plugin['xep_0196'].publish_gaming(name=name, server_address=address, - callback=dumb_callback, - block=False) + callback=dumb_callback) def command_invite(self, arg): """/invite <to> <room> [reason]""" @@ -834,22 +833,23 @@ def command_quit(self, arg=''): """ /quit """ + if not self.xmpp.is_connected(): + self.exit() + return if len(arg.strip()) != 0: msg = arg else: msg = None if config.get('enable_user_mood'): - self.xmpp.plugin['xep_0107'].stop(block=False) + self.xmpp.plugin['xep_0107'].stop() if config.get('enable_user_activity'): - self.xmpp.plugin['xep_0108'].stop(block=False) + self.xmpp.plugin['xep_0108'].stop() if config.get('enable_user_gaming'): - self.xmpp.plugin['xep_0196'].stop(block=False) + self.xmpp.plugin['xep_0196'].stop() self.save_config() self.plugin_manager.disable_plugins() self.disconnect(msg) - self.running = False - self.reset_curses() - sys.exit() + self.xmpp.add_event_handler("disconnected", self.exit, disposable=True) def command_destroy_room(self, arg=''): """ @@ -972,15 +972,13 @@ def command_adhoc(self, arg): if len(arg) > 1: return self.command_help('ad-hoc') elif arg: - jid = safeJID(arg[0]).server + jid = safeJID(arg[0]) else: return self.information('Please provide a jid', 'Error') list_tab = tabs.AdhocCommandsListTab(jid) self.add_tab(list_tab, True) cb = list_tab.on_list_received - self.xmpp.plugin['xep_0050'].get_commands(jid=jid, - local=False, - block=False, + self.xmpp.plugin['xep_0050'].get_commands(jid=jid, local=False, callback=cb) def command_self(self, arg=None): diff --git a/src/core/completions.py b/src/core/completions.py index 9549c13f..7d95321b 100644 --- a/src/core/completions.py +++ b/src/core/completions.py @@ -106,22 +106,7 @@ def completion_join(self, the_input): if the_input.last_completion: return the_input.new_completion([], 1, quotify=True) - if jid.server and not jid.user: - # no room was given: complete the node - try: - response = self.xmpp.plugin['xep_0030'].get_items(jid=jid.server, block=True, timeout=1) - except: - log.error('/join completion: Unable to get the list of rooms for %s', - jid.server, - exc_info=True) - response = None - if response: - items = response['disco_items'].get_items() - else: - return True - items = sorted('%s/%s' % (tup[0], jid.resource) for tup in items) - return the_input.new_completion(items, 1, quotify=True, override=True) - elif jid.user: + if jid.user: # we are writing the server: complete the server serv_list = [] for tab in self.get_tabs(tabs.MucTab): diff --git a/src/core/core.py b/src/core/core.py index 52199206..4daeed6c 100644 --- a/src/core/core.py +++ b/src/core/core.py @@ -9,7 +9,9 @@ import logging log = logging.getLogger(__name__) +import asyncio import collections +import shutil import curses import os import pipes @@ -19,7 +21,7 @@ from threading import Event from datetime import datetime from gettext import gettext as _ -from sleekxmpp.xmlstream.handler import Callback +from slixmpp.xmlstream.handler import Callback import bookmark import connection @@ -37,14 +39,13 @@ from config import config, firstrun from contact import Contact, Resource from daemon import Executor from fifo import Fifo -from keyboard import Keyboard from logger import logger from plugin_manager import PluginManager from roster import roster from size_manager import SizeManager from text_buffer import TextBuffer from theming import get_theme -from windows import g_lock +import keyboard from . import completions from . import commands @@ -71,7 +72,7 @@ class Core(object): self.running = True self.xmpp = singleton.Singleton(connection.Connection) self.xmpp.core = self - self.keyboard = Keyboard() + self.keyboard = keyboard.Keyboard() roster.set_node(self.xmpp.client_roster) decorators.refresh_wrapper.core = self self.paused = False @@ -108,6 +109,13 @@ class Core(object): self.size = SizeManager(self, windows.Win) + # Set to True whenever we consider that we have been disconnected + # from the server because of a legitimate reason (bad credentials, + # or explicit disconnect from the user for example), in that case we + # should not try to auto-reconnect, even if auto_reconnect is true + # in the user config. + self.legitimate_disconnect = False + # global commands, available from all tabs # a command is tuple of the form: # (the function executing the command. Takes a string as argument, @@ -123,6 +131,11 @@ class Core(object): del self.commands['status'] del self.commands['show'] + # A list of integers. For example if the user presses Alt+j, 2, 1, + # we will insert 2, then 1 in that list, and we will finally build + # the number 21 and use it with command_win, before clearing the + # list. + self.room_number_jump = [] self.key_func = KeyDict() # Key bindings associated with handlers # and pseudo-keys used to map actions below. @@ -188,9 +201,12 @@ class Core(object): self.key_func.update(key_func) # Add handlers + self.xmpp.add_event_handler('connecting', self.on_connecting) self.xmpp.add_event_handler('connected', self.on_connected) + self.xmpp.add_event_handler('connection_failed', self.on_failed_connection) self.xmpp.add_event_handler('disconnected', self.on_disconnected) - self.xmpp.add_event_handler('failed_auth', self.on_failed_auth) + self.xmpp.add_event_handler('stream_error', self.on_stream_error) + self.xmpp.add_event_handler('failed_all_auth', self.on_failed_all_auth) self.xmpp.add_event_handler('no_auth', self.on_no_auth) self.xmpp.add_event_handler("session_start", self.on_session_start) self.xmpp.add_event_handler("session_start", @@ -259,8 +275,6 @@ class Core(object): self.initial_joins = [] - self.timed_events = set() - self.connected_events = {} self.pending_invites = {} @@ -296,6 +310,8 @@ class Core(object): theming.update_themes_dir) self.add_configuration_handler("theme", self.on_theme_config_change) + self.add_configuration_handler("password", + self.on_password_change) self.add_configuration_handler("", self.on_any_config_change) @@ -374,6 +390,12 @@ class Core(object): self.information(error_msg, 'Warning') self.refresh_window() + def on_password_change(self, option, value): + """ + Set the new password in the slixmpp.ClientXMPP object + """ + self.xmpp.password = value + def sigusr_handler(self, num, stack): """ Handle SIGUSR1 (10) @@ -422,19 +444,14 @@ class Core(object): log.error("%s received. Exiting…", signals[sig]) if config.get('enable_user_mood'): - self.xmpp.plugin['xep_0107'].stop(block=False) + self.xmpp.plugin['xep_0107'].stop() if config.get('enable_user_activity'): - self.xmpp.plugin['xep_0108'].stop(block=False) + self.xmpp.plugin['xep_0108'].stop() if config.get('enable_user_gaming'): - self.xmpp.plugin['xep_0196'].stop(block=False) + self.xmpp.plugin['xep_0196'].stop() self.plugin_manager.disable_plugins() - self.disconnect('') - self.running = False - try: - self.reset_curses() - except: # too bad - pass - sys.exit() + self.disconnect('%s received' % signals.get(sig)) + self.xmpp.add_event_handler("disconnected", self.exit, disposable=True) def autoload_plugins(self): """ @@ -469,6 +486,11 @@ class Core(object): ' ask for help or tell us how great it is.'), _('Help')) self.refresh_window() + self.xmpp.plugin['xep_0012'].begin_idle(jid=self.xmpp.boundjid) + + def exit(self, event=None): + log.debug("exit(%s)" % (event,)) + asyncio.get_event_loop().stop() def on_exception(self, typ, value, trace): """ @@ -481,7 +503,28 @@ class Core(object): pass sys.__excepthook__(typ, value, trace) - def main_loop(self): + def sigwinch_handler(self): + """A work-around for ncurses resize stuff, which sucks. Normally, ncurses + catches SIGWINCH itself. In its signal handler, it updates the + windows structures (for example the size, etc) and it + ungetch(KEY_RESIZE). That way, the next time we call getch() we know + that a resize occured and we can act on it. BUT poezio doesn’t call + getch() until it knows it will return something. The problem is we + can’t know that, because stdin is not affected by this KEY_RESIZE + value (it is only inserted in a ncurses internal fifo that we can’t + access). + + The (ugly) solution is to handle SIGWINCH ourself, trigger the + change of the internal windows sizes stored in ncurses module, using + sizes that we get using shutil, ungetch the KEY_RESIZE value and + then call getch to handle the resize on poezio’s side properly. + """ + size = shutil.get_terminal_size() + curses.resizeterm(size.lines, size.columns) + curses.ungetch(curses.KEY_RESIZE) + self.on_input_readable() + + def on_input_readable(self): """ main loop waiting for the user to press a key """ @@ -528,39 +571,42 @@ class Core(object): res.append(current) return res - while self.running: - self.xmpp.plugin['xep_0012'].begin_idle(jid=self.xmpp.boundjid) - big_char_list = [replace_key_with_bound(key)\ - for key in self.read_keyboard()] - # whether to refresh after ALL keys have been handled - for char_list in separate_chars_from_bindings(big_char_list): - if self.paused: - self.current_tab().input.do_command(char_list[0]) - self.current_tab().input.prompt() - self.event.set() - 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() + log.debug("Input is readable.") + big_char_list = [replace_key_with_bound(key)\ + for key in self.read_keyboard()] + log.debug("Got from keyboard: %s", (big_char_list,)) + + # whether to refresh after ALL keys have been handled + for char_list in separate_chars_from_bindings(big_char_list): + if self.paused: + self.current_tab().input.do_command(char_list[0]) + self.current_tab().input.prompt() + self.event.set() + 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: - self.do_command(replace_line_breaks(char), False) + 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: - self.do_command(''.join(char_list), True) - self.doupdate() + self.do_command(replace_line_breaks(char), False) + else: + self.do_command(''.join(char_list), True) + if self.status.show not in ('xa', 'away'): + self.xmpp.plugin['xep_0012'].begin_idle(jid=self.xmpp.boundjid) + self.doupdate() def save_config(self): """ @@ -703,10 +749,21 @@ class Core(object): def do_command(self, key, raw): """ Execute the action associated with a key + + Or if keyboard.continuation_keys_callback is set, call it instead. See + the comment of this variable. """ if not key: return - return self.current_tab().on_input(key, raw) + if keyboard.continuation_keys_callback is not None: + # Reset the callback to None BEFORE calling it, because this + # callback MAY set a new callback itself, and we don’t want to + # erase it in that case + cb = keyboard.continuation_keys_callback + keyboard.continuation_keys_callback = None + cb(key) + else: + self.current_tab().on_input(key, raw) def try_execute(self, line): @@ -724,22 +781,13 @@ class Core(object): def remove_timed_event(self, event): """Remove an existing timed event""" - if event and event in self.timed_events: - self.timed_events.remove(event) + event.handler.cancel() 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 + event.handler = asyncio.get_event_loop().call_later(event.delay, + event.callback, + *event.args) ####################### XMPP-related actions ################################## @@ -779,12 +827,15 @@ class Core(object): Disconnect from remote server and correctly set the states of all parts of the client (for example, set the MucTabs as not joined, etc) """ + self.legitimate_disconnect = True msg = msg or '' for tab in self.get_tabs(tabs.MucTab): tab.command_part(msg) self.xmpp.disconnect() if reconnect: - self.xmpp.start() + # Add a one-time event to reconnect as soon as we are + # effectively disconnected + self.xmpp.add_event_handler('disconnected', lambda event: self.xmpp.connect(), disposable=True) def send_message(self, msg): """ @@ -815,8 +866,8 @@ class Core(object): self.xmpp.plugin['xep_0045'].invite(room, jid, reason=reason or '') - self.xmpp.plugin['xep_0030'].get_info(jid=jid, block=False, - timeout=5, callback=callback) + self.xmpp.plugin['xep_0030'].get_info(jid=jid, timeout=5, + callback=callback) def get_error_message(self, stanza, deprecated=False): """ @@ -1027,17 +1078,24 @@ class Core(object): 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 read_next_digit(digit): + try: + nb = int(digit) + except ValueError: + # If it is not a number, we do nothing. If it was the first + # one, we do not wait for a second one by re-setting the + # callback + self.room_number_jump.clear() + else: + self.room_number_jump.append(digit) + if len(self.room_number_jump) == 2: + arg = "".join(self.room_number_jump) + self.room_number_jump.clear() + self.command_win(arg) + else: + # We need to read more digits + keyboard.continuation_keys_callback = read_next_digit + keyboard.continuation_keys_callback = read_next_digit def go_to_roster(self): "Select the roster as the current tab" @@ -1505,41 +1563,39 @@ class Core(object): """ Resize the global_information_win only once at each resize. """ - with g_lock: - if self.information_win_size > tabs.Tab.height - 6: - self.information_win_size = tabs.Tab.height - 6 - if tabs.Tab.height < 6: - self.information_win_size = 0 - height = (tabs.Tab.height - 1 - self.information_win_size - - tabs.Tab.tab_win_height()) - self.information_win.resize(self.information_win_size, - tabs.Tab.width, - height, - 0) + if self.information_win_size > tabs.Tab.height - 6: + self.information_win_size = tabs.Tab.height - 6 + if tabs.Tab.height < 6: + self.information_win_size = 0 + height = (tabs.Tab.height - 1 - self.information_win_size + - tabs.Tab.tab_win_height()) + self.information_win.resize(self.information_win_size, + tabs.Tab.width, + height, + 0) def resize_global_info_bar(self): """ Resize the GlobalInfoBar only once at each resize """ - with g_lock: - height, width = self.stdscr.getmaxyx() - if config.get('enable_vertical_tab_list'): + height, width = self.stdscr.getmaxyx() + if config.get('enable_vertical_tab_list'): - if self.size.core_degrade_x: - return - try: - height, _ = self.stdscr.getmaxyx() - truncated_win = self.stdscr.subwin(height, - config.get('vertical_tab_list_size'), - 0, 0) - except: - log.error('Curses error on infobar resize', exc_info=True) - return - self.left_tab_win = windows.VerticalGlobalInfoBar(truncated_win) - elif not self.size.core_degrade_y: - self.tab_win.resize(1, tabs.Tab.width, - tabs.Tab.height - 2, 0) - self.left_tab_win = None + if self.size.core_degrade_x: + return + try: + height, _ = self.stdscr.getmaxyx() + truncated_win = self.stdscr.subwin(height, + config.get('vertical_tab_list_size'), + 0, 0) + except: + log.error('Curses error on infobar resize', exc_info=True) + return + self.left_tab_win = windows.VerticalGlobalInfoBar(truncated_win) + elif not self.size.core_degrade_y: + self.tab_win.resize(1, tabs.Tab.width, + tabs.Tab.height - 2, 0) + self.left_tab_win = None def add_message_to_text_buffer(self, buff, txt, time=None, nickname=None, history=None): @@ -1564,46 +1620,38 @@ class Core(object): Called when we want to resize the screen """ # If we have the tabs list on the left, we just give a truncated - # window to each Tab class, so the draw themself in the portion - # of the screen that the can occupy, and we draw the tab list - # on the left remaining space - with g_lock: - height, width = self.stdscr.getmaxyx() + # window to each Tab class, so they draw themself in the portion of + # the screen that they can occupy, and we draw the tab list on the + # remaining space, on the left + height, width = self.stdscr.getmaxyx() if (config.get('enable_vertical_tab_list') and not self.size.core_degrade_x): - with g_lock: - try: - scr = self.stdscr.subwin(0, - config.get('vertical_tab_list_size')) - except: - log.error('Curses error on resize', exc_info=True) - return + try: + scr = self.stdscr.subwin(0, + config.get('vertical_tab_list_size')) + except: + log.error('Curses error on resize', exc_info=True) + return else: scr = self.stdscr tabs.Tab.resize(scr) self.resize_global_info_bar() self.resize_global_information_win() - with g_lock: - for tab in self.tabs: - if config.get('lazy_resize'): - tab.need_resize = True - else: - tab.resize() - if self.tabs: - self.full_screen_redraw() + for tab in self.tabs: + if config.get('lazy_resize'): + tab.need_resize = True + else: + tab.resize() + if self.tabs: + self.full_screen_redraw() def read_keyboard(self): """ - Get the next keyboard key pressed and returns it. - get_user_input() has a timeout: it returns None when the timeout - occurs. In that case we do not return (we loop until we get - a non-None value), but we check for timed events instead. + Get the next keyboard key pressed and returns it. It blocks until + something can be read on stdin, this function must be called only if + there is something to read. No timeout ever occurs. """ - res = self.keyboard.get_user_input(self.stdscr) - while res is None: - self.check_timed_events() - res = self.keyboard.get_user_input(self.stdscr) - return res + return self.keyboard.get_user_input(self.stdscr) def escape_next_key(self): """ @@ -1883,9 +1931,11 @@ class Core(object): on_groupchat_presence = handlers.on_groupchat_presence on_failed_connection = handlers.on_failed_connection on_disconnected = handlers.on_disconnected - on_failed_auth = handlers.on_failed_auth + on_stream_error = handlers.on_stream_error + on_failed_all_auth = handlers.on_failed_all_auth on_no_auth = handlers.on_no_auth on_connected = handlers.on_connected + on_connecting = handlers.on_connecting on_session_start = handlers.on_session_start on_status_codes = handlers.on_status_codes on_groupchat_subject = handlers.on_groupchat_subject diff --git a/src/core/handlers.py b/src/core/handlers.py index dfcb3223..50dca216 100644 --- a/src/core/handlers.py +++ b/src/core/handlers.py @@ -5,16 +5,19 @@ XMPP-related handlers for the Core class import logging log = logging.getLogger(__name__) +import asyncio import curses +import functools import ssl +import sys import time from hashlib import sha1, sha512 from gettext import gettext as _ from os import path -from sleekxmpp import InvalidJID -from sleekxmpp.stanza import Message -from sleekxmpp.xmlstream.stanzabase import StanzaBase +from slixmpp import InvalidJID +from slixmpp.stanza import Message +from slixmpp.xmlstream.stanzabase import StanzaBase import bookmark import common @@ -49,47 +52,54 @@ def on_session_start_features(self, _): self.xmpp.plugin['xep_0280'].enable() self.xmpp.add_event_handler('carbon_received', self.on_carbon_received) self.xmpp.add_event_handler('carbon_sent', self.on_carbon_sent) - features = self.xmpp.plugin['xep_0030'].get_info(jid=self.xmpp.boundjid.domain, callback=callback, block=False) + + self.xmpp.plugin['xep_0030'].get_info(jid=self.xmpp.boundjid.domain, + callback=callback) def on_carbon_received(self, message): """ Carbon <received/> received """ + def ignore_message(recv): + log.debug('%s has category conference, ignoring carbon', + recv['from'].server) + def receive_message(recv): + recv['to'] = self.xmpp.boundjid.full + if recv['receipt']: + return self.on_receipt(recv) + self.on_normal_message(recv) + recv = message['carbon_received'] if (recv['from'].bare not in roster or - roster[recv['from'].bare].subscription == 'none'): - try: - if fixes.has_identity(self.xmpp, recv['from'].server, - identity='conference'): - log.debug('%s has category conference, ignoring carbon', - recv['from'].server) - return - except: - log.debug('Traceback when getting the identity of a server:', - exc_info=True) - recv['to'] = self.xmpp.boundjid.full - if recv['receipt']: - return self.on_receipt(recv) - self.on_normal_message(recv) + roster[recv['from'].bare].subscription == 'none'): + fixes.has_identity(self.xmpp, recv['from'].server, + identity='conference', + on_true=functools.partial(ignore_message, recv), + on_false=functools.partial(receive_message, recv)) + return + else: + receive_message(recv) def on_carbon_sent(self, message): """ Carbon <sent/> received """ + def ignore_message(sent): + log.debug('%s has category conference, ignoring carbon', + sent['to'].server) + def send_message(sent): + sent['from'] = self.xmpp.boundjid.full + self.on_normal_message(sent) + sent = message['carbon_sent'] if (sent['to'].bare not in roster or roster[sent['to'].bare].subscription == 'none'): - try: - if fixes.has_identity(self.xmpp, sent['to'].server, - identity='conference'): - log.debug('%s has category conference, ignoring carbon', - sent['to'].server) - return - except: - log.debug('Traceback when getting the identity of a server:', - exc_info=True) - sent['from'] = self.xmpp.boundjid.full - self.on_normal_message(sent) + fixes.has_identity(self.xmpp, sent['to'].server, + identity='conference', + on_true=functools.partial(ignore_message, sent), + on_false=functools.partial(send_message, sent)) + else: + send_message(sent) ### Invites ### @@ -171,7 +181,8 @@ def on_message(self, message): def on_normal_message(self, message): """ - When receiving "normal" messages (from someone in our roster) + When receiving "normal" messages (not a private message from a + muc participant) """ if message['type'] == 'error': return self.information(self.get_error_message(message, deprecated=True), 'Error') @@ -630,7 +641,7 @@ def on_chatstate_groupchat_conversation(self, message, state): Chatstate received in a MUC """ nick = message['mucnick'] - room_from = message.getMucroom() + room_from = message.get_mucroom() 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) @@ -828,27 +839,40 @@ def on_groupchat_presence(self, presence): ### Connection-related handlers ### -def on_failed_connection(self): +def on_failed_connection(self, error): """ We cannot contact the remote server """ - self.information(_("Connection to remote server failed"), _('Error')) + self.information(_("Connection to remote server failed: %s" % (error,)), _('Error')) def on_disconnected(self, event): """ When we are disconnected from remote server """ + # Stop the ping plugin. It would try to send stanza on regular basis + self.xmpp.plugin['xep_0199'].disable_keepalive() roster.modified() for tab in self.get_tabs(tabs.MucTab): tab.disconnect() self.information(_("Disconnected from server."), _('Error')) + if not self.legitimate_disconnect and config.get('auto_reconnect', False): + self.information(_("Auto-reconnecting."), _('Info')) + self.xmpp.connect() -def on_failed_auth(self, event): +def on_stream_error(self, event): + """ + When we receive a stream error + """ + if event and event['text']: + self.information(_('Stream error: %s') % event['text'], _('Error')) + +def on_failed_all_auth(self, event): """ Authentication failed """ self.information(_("Authentication failed (bad credentials?)."), _('Error')) + self.legitimate_disconnect = True def on_no_auth(self, event): """ @@ -856,6 +880,7 @@ def on_no_auth(self, event): """ self.information(_("Authentication failed, no login method available."), _('Error')) + self.legitimate_disconnect = True def on_connected(self, event): """ @@ -863,6 +888,12 @@ def on_connected(self, event): """ self.information(_("Connected to server."), 'Info') +def on_connecting(self, event): + """ + Just before we try to connect to the server + """ + self.legitimate_disconnect = False + def on_session_start(self, event): """ Called when we are connected and authenticated @@ -883,32 +914,42 @@ def on_session_start(self, event): self.events.trigger('send_normal_presence', pres) pres.send() bookmark.get_local() + def _join_initial_rooms(bookmarks): + """Join all rooms given in the iterator `bookmarks`""" + for bm in bookmarks: + if bm.autojoin or config.get('open_all_bookmarks'): + tab = self.get_tab_by_name(bm.jid, tabs.MucTab) + nick = bm.nick if bm.nick else self.own_nick + if not tab: + self.open_new_room(bm.jid, nick, False) + self.initial_joins.append(bm.jid) + histo_length = config.get('muc_history_length') + if histo_length == -1: + histo_length = None + if histo_length is not None: + histo_length = str(histo_length) + # do not join rooms that do not have autojoin + # but display them anyway + if bm.autojoin: + muc.join_groupchat(self, bm.jid, nick, + passwd=bm.password, + maxhistory=histo_length, + status=self.status.message, + show=self.status.show) + def _join_remote_only(): + remote_bookmarks = (bm for bm in bookmark.bookmarks if (bm.method in ("pep", "privatexml"))) + _join_initial_rooms(remote_bookmarks) if not self.xmpp.anon and config.get('use_remote_bookmarks'): - bookmark.get_remote(self.xmpp) - for bm in bookmark.bookmarks: - if bm.autojoin or config.get('open_all_bookmarks'): - tab = self.get_tab_by_name(bm.jid, tabs.MucTab) - nick = bm.nick if bm.nick else self.own_nick - if not tab: - self.open_new_room(bm.jid, nick, False) - self.initial_joins.append(bm.jid) - histo_length = config.get('muc_history_length') - if histo_length == -1: - histo_length = None - if histo_length is not None: - histo_length = str(histo_length) - # do not join rooms that do not have autojoin - # but display them anyway - if bm.autojoin: - muc.join_groupchat(self, bm.jid, nick, - passwd=bm.password, - maxhistory=histo_length, - status=self.status.message, - show=self.status.show) + bookmark.get_remote(self.xmpp, _join_remote_only) + # join all the available bookmarks. As of yet, this is just the local + # ones + _join_initial_rooms(bookmark.bookmarks) if config.get('enable_user_nick'): - self.xmpp.plugin['xep_0172'].publish_nick(nick=self.own_nick, callback=dumb_callback, block=False) + self.xmpp.plugin['xep_0172'].publish_nick(nick=self.own_nick, callback=dumb_callback) self.xmpp.plugin['xep_0115'].update_caps() + # Start the ping's plugin regular event + self.xmpp.set_keepalive_values() ### Other handlers ### @@ -974,7 +1015,7 @@ def on_groupchat_subject(self, message): Triggered when the topic is changed. """ nick_from = message['mucnick'] - room_from = message.getMucroom() + room_from = message.get_mucroom() tab = self.get_tab_by_name(room_from, tabs.MucTab) subject = message['subject'] if subject is None or not tab: @@ -1079,7 +1120,7 @@ def incoming_stanza(self, stanza): def validate_ssl(self, pem): """ - Check the server certificate using the sleekxmpp ssl_cert event + Check the server certificate using the slixmpp ssl_cert event """ if config.get('ignore_certificate'): return @@ -1115,19 +1156,31 @@ def validate_ssl(self, pem): 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: - self.event.wait() - self.current_tab().input = saved_input - self.paused = False - if input.value: - self.information('Setting new certificate: old: %s, new: %s' % (cert, sha2_found_cert), 'Info') - log.debug('Setting certificate to %s', sha2_found_cert) - if not config.silent_set('certificate', sha2_found_cert): - self.information(_('Unable to write in the config file'), 'Error') - else: - self.information('You refused to validate the certificate. You are now disconnected', 'Info') - self.xmpp.disconnect() + old_loop = asyncio.get_event_loop() + new_loop = asyncio.new_event_loop() + asyncio.set_event_loop(new_loop) + new_loop.add_reader(sys.stdin, self.on_input_readable) + future = asyncio.Future() + @asyncio.coroutine + def check_input(future): + while input.value is None: + yield from asyncio.sleep(0.01) + self.current_tab().input = saved_input + self.paused = False + if input.value: + self.information('Setting new certificate: old: %s, new: %s' % (cert, sha2_found_cert), 'Info') + log.debug('Setting certificate to %s', sha2_found_cert) + if not config.silent_set('certificate', sha2_found_cert): + self.information(_('Unable to write in the config file'), 'Error') + else: + self.information('You refused to validate the certificate. You are now disconnected', 'Info') + self.xmpp.disconnect() + new_loop.stop() + asyncio.set_event_loop(old_loop) + asyncio.async(check_input(future)) + new_loop.run_forever() + + else: log.debug('First time. Setting certificate to %s', sha2_found_cert) if not config.silent_set('certificate', sha2_found_cert): |