diff options
Diffstat (limited to 'poezio/core/core.py')
-rw-r--r-- | poezio/core/core.py | 1010 |
1 files changed, 400 insertions, 610 deletions
diff --git a/poezio/core/core.py b/poezio/core/core.py index 525d02a6..6582402d 100644 --- a/poezio/core/core.py +++ b/poezio/core/core.py @@ -5,6 +5,8 @@ of everything; it also contains global commands, completions and event handlers but those are defined in submodules in order to avoir cluttering this file. """ +from __future__ import annotations + import logging import asyncio import curses @@ -13,29 +15,47 @@ import pipes import sys import shutil import time -import uuid from collections import defaultdict -from typing import Callable, Dict, List, Optional, Set, Tuple, Type +from typing import ( + Any, + Callable, + Dict, + List, + Optional, + Set, + Tuple, + Type, + TypeVar, + TYPE_CHECKING, +) from xml.etree import ElementTree as ET -from functools import partial +from pathlib import Path -from slixmpp import JID, InvalidJID +from slixmpp import Iq, JID, InvalidJID from slixmpp.util import FileSystemPerJidCache +from slixmpp.xmlstream.xmlstream import InvalidCABundle from slixmpp.xmlstream.handler import Callback -from slixmpp.exceptions import IqError, IqTimeout +from slixmpp.exceptions import IqError, IqTimeout, XMPPError from poezio import connection from poezio import decorators from poezio import events -from poezio import multiuserchat as muc -from poezio import tabs -from poezio import mam from poezio import theming from poezio import timed_events from poezio import windows - -from poezio.bookmarks import BookmarkList -from poezio.config import config, firstrun +from poezio import utils + +from poezio.bookmarks import ( + BookmarkList, + Bookmark, +) +from poezio.tabs import ( + Tab, XMLTab, ChatTab, ConversationTab, PrivateTab, MucTab, OneToOneTab, + GapTab, RosterInfoTab, StaticConversationTab, DataFormsTab, + DynamicConversationTab, STATE_PRIORITY +) +from poezio.common import get_error_message +from poezio.config import config from poezio.contact import Contact, Resource from poezio.daemon import Executor from poezio.fifo import Fifo @@ -46,45 +66,92 @@ from poezio.size_manager import SizeManager from poezio.user import User from poezio.text_buffer import TextBuffer from poezio.timed_events import DelayedEvent -from poezio.theming import get_theme from poezio import keyboard, xdg from poezio.core.completions import CompletionCore from poezio.core.tabs import Tabs from poezio.core.commands import CommandCore +from poezio.core.command_defs import get_commands from poezio.core.handlers import HandlerCore -from poezio.core.structs import POSSIBLE_SHOW, DEPRECATED_ERRORS, \ - ERROR_AND_STATUS_CODES, Command, Status +from poezio.core.structs import ( + Command, + Status, + POSSIBLE_SHOW, +) + +from poezio.ui.types import ( + PersistentInfoMessage, + UIMessage, +) + +if TYPE_CHECKING: + from _curses import _CursesWindow # pylint: disable=no-name-in-module log = logging.getLogger(__name__) +T = TypeVar('T', bound=Tab) + class Core: """ “Main” class of poezion """ - def __init__(self): + custom_version: str + firstrun: bool + completion: CompletionCore + command: CommandCore + handler: HandlerCore + bookmarks: BookmarkList + status: Status + commands: Dict[str, Command] + room_number_jump: List[str] + initial_joins: List[JID] + pending_invites: Dict[str, str] + configuration_change_handlers: Dict[str, List[Callable[..., None]]] + own_nick: str + connection_time: float + xmpp: connection.Connection + avatar_cache: FileSystemPerJidCache + plugins_autoloaded: bool + previous_tab_nb: int + tabs: Tabs + size: SizeManager + plugin_manager: PluginManager + events: events.EventHandler + legitimate_disconnect: bool + information_buffer: TextBuffer + information_win_size: int + stdscr: Optional[_CursesWindow] + xml_buffer: TextBuffer + xml_tab: Optional[XMLTab] + last_stream_error: Optional[Tuple[float, XMPPError]] + remote_fifo: Optional[Fifo] + key_func: KeyDict + tab_win: windows.GlobalInfoBar + left_tab_win: Optional[windows.VerticalGlobalInfoBar] + + def __init__(self, custom_version: str, firstrun: bool): self.completion = CompletionCore(self) self.command = CommandCore(self) self.handler = HandlerCore(self) + self.firstrun = firstrun # All uncaught exception are given to this callback, instead # of being displayed on the screen and exiting the program. sys.excepthook = self.on_exception self.connection_time = time.time() self.last_stream_error = None self.stdscr = None - status = config.get('status') - status = POSSIBLE_SHOW.get(status, None) - self.status = Status(show=status, message=config.get('status_message')) - self.running = True - self.xmpp = connection.Connection() + status = config.getstr('status') + status = POSSIBLE_SHOW.get(status) or '' + self.status = Status(show=status, message=config.getstr('status_message')) + self.custom_version = custom_version + self.xmpp = connection.Connection(custom_version) self.xmpp.core = self self.keyboard = keyboard.Keyboard() roster.set_node(self.xmpp.client_roster) decorators.refresh_wrapper.core = self self.bookmarks = BookmarkList() - self.debug = False self.remote_fifo = None self.avatar_cache = FileSystemPerJidCache( str(xdg.CACHE_HOME), 'avatars', binary=True) @@ -92,13 +159,8 @@ class Core: # that are displayed in almost all tabs, in an # information window. self.information_buffer = TextBuffer() - self.information_win_size = config.get( - 'info_win_height', section='var') - self.information_win = windows.TextWin(300) - self.information_buffer.add_window(self.information_win) - self.left_tab_win = None + self.information_win_size = config.getint('info_win_height', section='var') - self.tab_win = windows.GlobalInfoBar(self) # Whether the XML tab is opened self.xml_tab = None self.xml_buffer = TextBuffer() @@ -108,14 +170,13 @@ class Core: self.events = events.EventHandler() self.events.add_event_handler('tab_change', self.on_tab_change) - self.tabs = Tabs(self.events) + self.tabs = Tabs(self.events, GapTab()) self.previous_tab_nb = 0 - own_nick = config.get('default_nick') - own_nick = own_nick or self.xmpp.boundjid.user - own_nick = own_nick or os.environ.get('USER') - own_nick = own_nick or 'poezio' - self.own_nick = own_nick + self.own_nick: str = ( + config.getstr('default_nick') or self.xmpp.boundjid.user or + os.environ.get('USER') or 'poezio_user' + ) self.size = SizeManager(self) @@ -202,6 +263,7 @@ class Core: '_show_plugins': self.command.plugins, '_show_xmltab': self.command.xml_tab, '_toggle_pane': self.toggle_left_pane, + "_go_to_room_name": self.go_to_room_name, ###### status actions ###### '_available': lambda: self.command.status('available'), '_away': lambda: self.command.status('away'), @@ -209,12 +271,12 @@ class Core: '_dnd': lambda: self.command.status('dnd'), '_xa': lambda: self.command.status('xa'), ##### Custom actions ######## - '_exc_': self.try_execute, } self.key_func.update(key_func) + self.key_func.try_execute = self.try_execute # Add handlers - xmpp_event_handlers = [ + xmpp_event_handlers: List[Tuple[str, Callable[..., Any]]] = [ ('attention', self.handler.on_attention), ('carbon_received', self.handler.on_carbon_received), ('carbon_sent', self.handler.on_carbon_sent), @@ -268,35 +330,20 @@ class Core: for name, handler in xmpp_event_handlers: self.xmpp.add_event_handler(name, handler) - if config.get('enable_avatars'): + if config.getbool('enable_avatars'): self.xmpp.add_event_handler("vcard_avatar_update", self.handler.on_vcard_avatar) self.xmpp.add_event_handler("avatar_metadata_publish", self.handler.on_0084_avatar) - if config.get('enable_user_tune'): - self.xmpp.add_event_handler("user_tune_publish", - self.handler.on_tune_event) - if config.get('enable_user_nick'): + if config.getbool('enable_user_nick'): self.xmpp.add_event_handler("user_nick_publish", self.handler.on_nick_received) - if config.get('enable_user_mood'): - self.xmpp.add_event_handler("user_mood_publish", - self.handler.on_mood_event) - if config.get('enable_user_activity'): - self.xmpp.add_event_handler("user_activity_publish", - self.handler.on_activity_event) - if config.get('enable_user_gaming'): - self.xmpp.add_event_handler("user_gaming_publish", - self.handler.on_gaming_event) - all_stanzas = Callback('custom matcher', connection.MatchAll(None), self.handler.incoming_stanza) self.xmpp.register_handler(all_stanzas) self.initial_joins = [] - self.connected_events = {} - self.pending_invites = {} # a dict of the form {'config_option': [list, of, callbacks]} @@ -312,13 +359,12 @@ class Core: # The callback takes two argument: the config option, and the new # value self.configuration_change_handlers = defaultdict(list) - config_handlers = [ + config_handlers: List[Tuple[str, Callable[..., Any]]] = [ ('', self.on_any_config_change), ('ack_message_receipts', self.on_ack_receipts_config_change), ('connection_check_interval', self.xmpp.set_keepalive_values), ('connection_timeout_delay', self.xmpp.set_keepalive_values), ('create_gaps', self.on_gaps_config_change), - ('deterministic_nick_colors', self.on_nick_determinism_changed), ('enable_carbons', self.on_carbons_switch), ('enable_vertical_tab_list', self.on_vertical_tab_list_config_change), @@ -329,6 +375,7 @@ class Core: ('plugins_dir', self.plugin_manager.on_plugins_dir_change), ('request_message_receipts', self.on_request_receipts_config_change), + ('show_timestamps', self.on_show_timestamps_changed), ('theme', self.on_theme_config_change), ('themes_dir', theming.update_themes_dir), ('use_bookmarks_method', self.on_bookmarks_method_config_change), @@ -338,7 +385,14 @@ class Core: for option, handler in config_handlers: self.add_configuration_handler(option, handler) - def on_tab_change(self, old_tab: tabs.Tab, new_tab: tabs.Tab): + def _create_windows(self): + """Create the windows (delayed after curses init)""" + self.information_win = windows.TextWin(300) + self.information_buffer.add_window(self.information_win) + self.left_tab_win = None + self.tab_win = windows.GlobalInfoBar(self) + + def on_tab_change(self, old_tab: Tab, new_tab: Tab): """Whenever the current tab changes, change focus and refresh""" old_tab.on_lose_focus() new_tab.on_gain_focus() @@ -379,6 +433,12 @@ class Core: """ self.call_for_resize() + def on_show_timestamps_changed(self, option, value): + """ + Called when the show_timestamps option changes + """ + self.call_for_resize(ui_config_changed=True) + def on_bookmarks_method_config_change(self, option, value): """ Called when the use_bookmarks_method option changes @@ -386,7 +446,9 @@ class Core: if value not in ('pep', 'privatexml'): return self.bookmarks.preferred = value - self.bookmarks.save(self.xmpp, core=self) + asyncio.create_task( + self.bookmarks.save(self.xmpp, core=self) + ) def on_gaps_config_change(self, option, value): """ @@ -430,14 +492,6 @@ class Core: """ self.xmpp.password = value - def on_nick_determinism_changed(self, option, value): - """If we change the value to true, we call /recolor on all the MucTabs, to - make the current nick colors reflect their deterministic value. - """ - if value.lower() == "true": - for tab in self.get_tabs(tabs.MucTab): - tab.command_recolor('') - def on_carbons_switch(self, option, value): """Whenever the user enables or disables carbons using /set, we should inform the server immediately, this way we do not require a restart @@ -501,12 +555,6 @@ class Core: } log.error("%s received. Exiting…", signals[sig]) - if config.get('enable_user_mood'): - self.xmpp.plugin['xep_0107'].stop() - if config.get('enable_user_activity'): - self.xmpp.plugin['xep_0108'].stop() - if config.get('enable_user_gaming'): - self.xmpp.plugin['xep_0196'].stop() self.plugin_manager.disable_plugins() self.disconnect('%s received' % signals.get(sig)) self.xmpp.add_event_handler("disconnected", self.exit, disposable=True) @@ -515,13 +563,13 @@ class Core: """ Load the plugins on startup. """ - plugins = config.get('plugins_autoload') + plugins = config.getstr('plugins_autoload') if ':' in plugins: for plugin in plugins.split(':'): - self.plugin_manager.load(plugin) + self.plugin_manager.load(plugin, unload_first=False) else: for plugin in plugins.split(): - self.plugin_manager.load(plugin) + self.plugin_manager.load(plugin, unload_first=False) self.plugins_autoloaded = True def start(self): @@ -530,12 +578,20 @@ class Core: """ self.stdscr = curses.initscr() self._init_curses(self.stdscr) + windows.base_wins.TAB_WIN = self.stdscr + self._create_windows() self.call_for_resize() - default_tab = tabs.RosterInfoTab(self) + default_tab = RosterInfoTab(self) default_tab.on_gain_focus() self.tabs.append(default_tab) self.information('Welcome to poezio!', 'Info') - if firstrun: + if curses.COLORS < 256: + self.information( + 'Your terminal does not appear to support 256 colors, the UI' + ' colors will probably be ugly', + 'Error', + ) + if self.firstrun: self.information( 'It seems that it is the first time you start poezio.\n' 'The online help is here https://doc.poez.io/\n\n' @@ -563,7 +619,7 @@ class Core: pass sys.__excepthook__(typ, value, trace) - def sigwinch_handler(self): + def sigwinch_handler(self, *args): """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 @@ -605,7 +661,7 @@ class Core: except ValueError: pass else: - if self.tabs.current_tab.nb == nb and config.get( + if self.tabs.current_tab.nb == nb and config.getbool( 'go_to_previous_tab_on_alt_number'): self.go_to_previous_tab() else: @@ -618,10 +674,28 @@ class Core: 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_0319'].idle() self.doupdate() + def loop_exception_handler(self, loop, context) -> None: + """Do not log unhandled iq errors and timeouts""" + handled_exceptions = (IqError, IqTimeout, InvalidCABundle) + if not isinstance(context['exception'], handled_exceptions): + loop.default_exception_handler(context) + elif isinstance(context['exception'], InvalidCABundle): + paths = context['exception'].path + error = ( + 'Poezio could not find a valid CA bundle file automatically. ' + 'Ensure the ca_cert_path configuration is set to a valid ' + 'CA bundle path, generally provided by the \'ca-certificates\' ' + 'package in your distribution.' + ) + if isinstance(paths, (str, Path)): + # error += '\nFound the following value: {path}'.format(path=str(path)) + paths = [paths] + if paths is not None: + error += f"\nThe following values were tried: {str([str(s) for s in paths])}" + self.information(error, 'Error') + def save_config(self): """ Save config in the file just before exit @@ -659,7 +733,7 @@ class Core: Messages are namedtuples of the form ('txt nick_color time str_time nickname user') """ - if not isinstance(self.tabs.current_tab, tabs.ChatTab): + if not isinstance(self.tabs.current_tab, ChatTab): return None return self.tabs.current_tab.get_conversation_messages() @@ -716,9 +790,9 @@ class Core: work. If you try to do anything else, your |, [, <<, etc will be interpreted as normal command arguments, not shell special tokens. """ - if config.get('exec_remote'): + if config.getbool('exec_remote'): # We just write the command in the fifo - fifo_path = config.get('remote_fifo_path') + fifo_path = config.getstr('remote_fifo_path') filename = os.path.join(fifo_path, 'poezio.fifo') if not self.remote_fifo: try: @@ -790,16 +864,18 @@ class Core: def remove_timed_event(self, event: DelayedEvent) -> None: """Remove an existing timed event""" - event.handler.cancel() + if event.handler is not None: + event.handler.cancel() def add_timed_event(self, event: DelayedEvent) -> None: """Add a new timed event""" event.handler = asyncio.get_event_loop().call_later( - event.delay, event.callback, *event.args) + event.delay, event.callback, *event.args + ) ####################### XMPP-related actions ################################## - def get_status(self) -> str: + def get_status(self) -> Status: """ Get the last status that was previously set """ @@ -812,7 +888,7 @@ class Core: or to use it when joining a new muc) """ self.status = Status(show=pres, message=msg) - if config.get('save_status'): + if config.getbool('save_status'): ok = config.silent_set('status', pres if pres else '') msg = msg.replace('\n', '|') if msg else '' ok = ok and config.silent_set('status_message', msg) @@ -827,7 +903,7 @@ class Core: or the default nickname """ bm = self.bookmarks[room_name] - if bm: + if bm and bm.nick: return bm.nick return self.own_nick @@ -840,7 +916,7 @@ class Core: if reconnect: self.xmpp.reconnect(wait=0.0, reason=msg) else: - for tab in self.get_tabs(tabs.MucTab): + for tab in self.get_tabs(MucTab): tab.leave_room(msg) self.xmpp.disconnect(reason=msg) @@ -850,32 +926,48 @@ class Core: conversation. Returns False if the current tab is not a conversation tab """ - if not isinstance(self.tabs.current_tab, tabs.ChatTab): + if not isinstance(self.tabs.current_tab, ChatTab): return False - self.tabs.current_tab.command_say(msg) + asyncio.ensure_future( + self.tabs.current_tab.command_say(msg) + ) return True - def invite(self, jid: JID, room: JID, reason: Optional[str] = None) -> None: + async def invite(self, jid: JID, room: JID, reason: Optional[str] = None, force_mediated: bool = False) -> bool: """ Checks if the sender supports XEP-0249, then send an invitation, or a mediated one if it does not. TODO: allow passwords """ + features = set() - def callback(iq): - if not iq: - return - if 'jabber:x:conference' in iq['disco_info'].get_features(): - self.xmpp.plugin['xep_0249'].send_invitation( - jid, room, reason=reason) - else: # fallback - self.xmpp.plugin['xep_0045'].invite( - room, jid, reason=reason or '') - - self.xmpp.plugin['xep_0030'].get_info( - jid=jid, timeout=5, callback=callback) + # force mediated: act as if the other entity does not + # support direct invites + if not force_mediated: + try: + iq = await self.xmpp.plugin['xep_0030'].get_info( + jid=jid, + timeout=5, + ) + features = iq['disco_info'].get_features() + except (IqError, IqTimeout): + pass + supports_direct = 'jabber:x:conference' in features + if supports_direct: + self.xmpp.plugin['xep_0249'].send_invitation( + jid=jid, + roomjid=room, + reason=reason + ) + else: # fallback + self.xmpp.plugin['xep_0045'].invite( + jid=jid, + room=room, + reason=reason or '', + ) + return True - def _impromptu_room_form(self, room): + def _impromptu_room_form(self, room) -> Iq: fields = [ ('hidden', 'FORM_TYPE', 'http://jabber.org/protocol/muc#roomconfig'), ('boolean', 'muc#roomconfig_changesubject', True), @@ -936,82 +1028,78 @@ class Core: ) return - nick = self.own_nick - localpart = uuid.uuid4().hex - room_str = '{!s}@{!s}'.format(localpart, default_muc) - try: - room = JID(room_str) - except InvalidJID: + # Retries generating a name until we find a non-existing room. + # Abort otherwise. + retries = 3 + while retries > 0: + localpart = utils.pronounceable() + room_str = f'{localpart}@{default_muc}' + try: + room = JID(room_str) + except InvalidJID: + self.information( + f'The generated XMPP address is invalid: {room_str}', + 'Error' + ) + return None + + try: + iq = await self.xmpp['xep_0030'].get_info( + jid=room, + cached=False, + ) + except IqTimeout: + pass + except IqError as exn: + if exn.etype == 'cancel' and exn.condition == 'item-not-found': + log.debug('Found empty room for /impromptu') + break + + retries = retries - 1 + + if retries == 0: self.information( - 'The generated XMPP address is invalid: {!s}'.format(room_str), - 'Error' + 'Couldn\'t generate a room name that isn\'t already used.', + 'Error', ) return None - self.open_new_room(room, nick).join() - iq = self._impromptu_room_form(room) - try: - await iq.send() - except (IqError, IqTimeout): - self.information('Failed to configure impromptu room.', 'Info') - # TODO: destroy? leave room. - return None + self.open_new_room(room, self.own_nick).join() - self.information('Room %s created' % room, 'Info') + async def configure_and_invite(_presence): + iq = self._impromptu_room_form(room) + try: + await iq.send() + except (IqError, IqTimeout): + self.information('Failed to configure impromptu room.', 'Info') + # TODO: destroy? leave room. + return None - for jid in jids: - self.invite(jid, room) + self.information(f'Room {room} created', 'Info') - def get_error_message(self, stanza, deprecated: bool = False): - """ - Takes a stanza of the form <message type='error'><error/></message> - and return a well formed string containing error information - """ - sender = stanza['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 + for jid in jids: + await self.invite(jid, room, force_mediated=True) + jids_str = ', '.join(jids) + self.information(f'Invited {jids_str} to {room.bare}', 'Info') + + self.xmpp.add_event_handler( + f'muc::{room.bare}::groupchat_subject', + configure_and_invite, + disposable=True, + ) ####################### Tab logic-related things ############################## ### Tab getters ### - def get_tabs(self, cls: Type[tabs.Tab] = None) -> List[tabs.Tab]: + def get_tabs(self, cls: Type[T]) -> List[T]: "Get all the tabs of a type" - if cls is None: - return self.tabs.get_tabs() return self.tabs.by_class(cls) def get_conversation_by_jid(self, jid: JID, create: bool = True, - fallback_barejid: bool = True) -> Optional[tabs.ChatTab]: + fallback_barejid: bool = True) -> Optional[ChatTab]: """ From a JID, get the tab containing the conversation with it. If none already exist, and create is "True", we create it @@ -1023,16 +1111,17 @@ class Core: jid = JID(jid) # We first check if we have a static conversation opened # with this precise resource + conversation: Optional[ConversationTab] conversation = self.tabs.by_name_and_class(jid.full, - tabs.StaticConversationTab) + StaticConversationTab) if jid.bare == jid.full and not conversation: conversation = self.tabs.by_name_and_class( - jid.full, tabs.DynamicConversationTab) + jid.full, DynamicConversationTab) if not conversation and fallback_barejid: # If not, we search for a conversation with the bare jid conversation = self.tabs.by_name_and_class( - jid.bare, tabs.DynamicConversationTab) + jid.bare, DynamicConversationTab) if not conversation: if create: # We create a dynamic conversation with the bare Jid if @@ -1044,7 +1133,7 @@ class Core: conversation = None return conversation - def add_tab(self, new_tab: tabs.Tab, focus: bool = False) -> None: + def add_tab(self, new_tab: Tab, focus: bool = False) -> None: """ Appends the new_tab in the tab list and focus it if focus==True @@ -1059,21 +1148,21 @@ class Core: returns False if it could not move the tab, True otherwise """ return self.tabs.insert_tab(old_pos, new_pos, - config.get('create_gaps')) + config.getbool('create_gaps')) ### Move actions (e.g. go to next room) ### - def rotate_rooms_right(self, args=None) -> None: + def rotate_rooms_right(self) -> None: """ rotate the rooms list to the right """ - self.tabs.next() + self.tabs.next() # pylint: disable=not-callable - def rotate_rooms_left(self, args=None) -> None: + def rotate_rooms_left(self) -> None: """ rotate the rooms list to the right """ - self.tabs.prev() + self.tabs.prev() # pylint: disable=not-callable def go_to_room_number(self) -> None: """ @@ -1101,6 +1190,34 @@ class Core: keyboard.continuation_keys_callback = read_next_digit + def go_to_room_name(self) -> None: + room_name_jump = [] + + def read_next_letter(s) -> None: + nonlocal room_name_jump + room_name_jump.append(s) + any_matched, unique_tab = self.tabs.find_by_unique_prefix( + "".join(room_name_jump) + ) + + if not any_matched: + return + + if unique_tab is not None: + self.tabs.set_current_tab(unique_tab) + # NOTE: returning here means that as soon as the tab is + # matched, normal input resumes. If we do *not* return here, + # any further characters matching the prefix of the tab will + # be swallowed (and a lot of tab switching will happen...), + # until a non-matching character or escape or something is + # pressed. + # This behaviour *may* be desirable. + return + + keyboard.continuation_keys_callback = read_next_letter + + keyboard.continuation_keys_callback = read_next_letter + def go_to_roster(self) -> None: "Select the roster as the current tab" self.tabs.set_current_tab(self.tabs.first()) @@ -1112,11 +1229,11 @@ class Core: def go_to_important_room(self) -> None: """ Go to the next room with activity, in the order defined in the - dict tabs.STATE_PRIORITY + dict STATE_PRIORITY """ # shortcut - priority = tabs.STATE_PRIORITY - tab_refs = {} # type: Dict[str, List[tabs.Tab]] + priority = STATE_PRIORITY + tab_refs: Dict[str, List[Tab]] = {} # put all the active tabs in a dict of lists by state for tab in self.tabs.get_tabs(): if not tab: @@ -1141,7 +1258,7 @@ class Core: def focus_tab_named(self, tab_name: str, - type_: Type[tabs.Tab] = None) -> bool: + type_: Type[Tab] = None) -> bool: """Returns True if it found a tab to focus on""" if type_ is None: tab = self.tabs.by_name(tab_name) @@ -1152,23 +1269,24 @@ class Core: return True return False - def focus_tab(self, tab: tabs.Tab) -> bool: + def focus_tab(self, tab: Tab) -> bool: """Focus a tab""" return self.tabs.set_current_tab(tab) ### Opening actions ### def open_conversation_window(self, jid: JID, - focus=True) -> tabs.ConversationTab: + focus=True) -> ConversationTab: """ Open a new conversation tab and focus it if needed. If a resource is provided, we open a StaticConversationTab, else a DynamicConversationTab """ + new_tab: ConversationTab if jid.resource: - new_tab = tabs.StaticConversationTab(self, jid) + new_tab = StaticConversationTab(self, jid) else: - new_tab = tabs.DynamicConversationTab(self, jid) + new_tab = DynamicConversationTab(self, jid) if not focus: new_tab.state = "private" self.add_tab(new_tab, focus) @@ -1176,41 +1294,41 @@ class Core: return new_tab def open_private_window(self, room_name: str, user_nick: str, - focus=True) -> Optional[tabs.PrivateTab]: + focus=True) -> Optional[PrivateTab]: """ Open a Private conversation in a MUC and focus if needed. """ complete_jid = room_name + '/' + user_nick # if the room exists, focus it and return - for tab in self.get_tabs(tabs.PrivateTab): + for tab in self.get_tabs(PrivateTab): if tab.name == complete_jid: self.tabs.set_current_tab(tab) return tab # create the new tab - tab = self.tabs.by_name_and_class(room_name, tabs.MucTab) - if not tab: + muc_tab = self.tabs.by_name_and_class(room_name, MucTab) + if not muc_tab: return None - new_tab = tabs.PrivateTab(self, complete_jid, tab.own_nick) + tab = PrivateTab(self, complete_jid, muc_tab.own_nick) if hasattr(tab, 'directed_presence'): - new_tab.directed_presence = tab.directed_presence + tab.directed_presence = tab.directed_presence if not focus: - new_tab.state = "private" + tab.state = "private" # insert it in the tabs - self.add_tab(new_tab, focus) + self.add_tab(tab, focus) self.refresh_window() - tab.privates.append(new_tab) - return new_tab + muc_tab.privates.append(tab) + return tab def open_new_room(self, - room: str, + room: JID, nick: str, *, password: Optional[str] = None, - focus=True) -> tabs.MucTab: + focus=True) -> MucTab: """ Open a new tab.MucTab containing a muc Room, using the specified nick """ - new_tab = tabs.MucTab(self, room, nick, password=password) + new_tab = MucTab(self, room, nick, password=password) self.add_tab(new_tab, focus) self.refresh_window() return new_tab @@ -1222,7 +1340,7 @@ class Core: The callback are called with the completed form as parameter in addition with kwargs """ - form_tab = tabs.DataFormsTab(self, form, on_cancel, on_send, kwargs) + form_tab = DataFormsTab(self, form, on_cancel, on_send, kwargs) self.add_tab(form_tab, True) ### Modifying actions ### @@ -1234,7 +1352,7 @@ class Core: with him/her """ tab = self.tabs.by_name_and_class('%s/%s' % (room_name, old_nick), - tabs.PrivateTab) + PrivateTab) if tab: tab.rename_user(old_nick, user) @@ -1245,7 +1363,7 @@ class Core: private conversation """ tab = self.tabs.by_name_and_class('%s/%s' % (room_name, user.nick), - tabs.PrivateTab) + PrivateTab) if tab: tab.user_left(status_message, user) @@ -1255,7 +1373,7 @@ class Core: private conversation """ tab = self.tabs.by_name_and_class('%s/%s' % (room_name, nick), - tabs.PrivateTab) + PrivateTab) if tab: tab.user_rejoined(nick) @@ -1267,7 +1385,7 @@ class Core: """ if reason is None: reason = '\x195}You left the room\x193}' - for tab in self.get_tabs(tabs.PrivateTab): + for tab in self.get_tabs(PrivateTab): if tab.name.startswith(room_name): tab.deactivate(reason=reason) @@ -1278,28 +1396,28 @@ class Core: """ if reason is None: reason = '\x195}You joined the room\x193}' - for tab in self.get_tabs(tabs.PrivateTab): + for tab in self.get_tabs(PrivateTab): if tab.name.startswith(room_name): tab.activate(reason=reason) - def on_user_changed_status_in_private(self, jid: JID, status: str) -> None: - tab = self.tabs.by_name_and_class(jid, tabs.ChatTab) + def on_user_changed_status_in_private(self, jid: JID, status: Status) -> None: + tab = self.tabs.by_name_and_class(jid, OneToOneTab) if tab is not None: # display the message in private tab.update_status(status) - def close_tab(self, to_close: tabs.Tab = None) -> None: + def close_tab(self, to_close: Tab = None) -> None: """ Close the given tab. If None, close the current one """ was_current = to_close is None tab = to_close or self.tabs.current_tab - if isinstance(tab, tabs.RosterInfoTab): + if isinstance(tab, RosterInfoTab): return # The tab 0 should NEVER be closed tab.on_close() del tab.key_func # Remove self references del tab.commands # and make the object collectable - self.tabs.delete(tab, gap=config.get('create_gaps')) + self.tabs.delete(tab, gap=config.getbool('create_gaps')) logger.close(tab.name) if was_current: self.tabs.current_tab.on_gain_focus() @@ -1315,9 +1433,9 @@ class Core: Search for a ConversationTab with the given jid (full or bare), if yes, add the given message to it """ - tab = self.tabs.by_name_and_class(jid, tabs.ConversationTab) + tab = self.tabs.by_name_and_class(jid, ConversationTab) if tab is not None: - tab.add_message(msg, typ=2) + tab.add_message(PersistentInfoMessage(msg)) if self.tabs.current_tab is tab: self.refresh_window() @@ -1325,36 +1443,36 @@ class Core: def doupdate(self) -> None: "Do a curses update" - if not self.running: - return curses.doupdate() def information(self, msg: str, typ: str = '') -> bool: """ Displays an informational message in the "Info" buffer """ - filter_types = config.get('information_buffer_type_filter').split(':') + filter_types = config.getlist('information_buffer_type_filter') if typ.lower() in filter_types: log.debug( 'Did not show the message:\n\t%s> %s \n\tdue to ' 'information_buffer_type_filter configuration', typ, msg) return False - filter_messages = config.get('filter_info_messages').split(':') + filter_messages = config.getlist('filter_info_messages') for words in filter_messages: if words and words in msg: log.debug( 'Did not show the message:\n\t%s> %s \n\tdue to filter_info_messages configuration', typ, msg) return False - colors = get_theme().INFO_COLORS - color = colors.get(typ.lower(), colors.get('default', None)) nb_lines = self.information_buffer.add_message( - msg, nickname=typ, nick_color=color) - popup_on = config.get('information_buffer_popup_on').split() - if isinstance(self.tabs.current_tab, tabs.RosterInfoTab): + UIMessage( + txt=msg, + level=typ, + ) + ) + popup_on = config.getlist('information_buffer_popup_on') + if isinstance(self.tabs.current_tab, RosterInfoTab): self.refresh_window() elif typ != '' and typ.lower() in popup_on: - popup_time = config.get('popup_time') + (nb_lines - 1) * 2 + popup_time = config.getint('popup_time') + (nb_lines - 1) * 2 self._pop_information_win_up(nb_lines, popup_time) else: if self.information_win_size != 0: @@ -1502,7 +1620,7 @@ class Core: Scroll the information buffer up """ self.information_win.scroll_up(self.information_win.height) - if not isinstance(self.tabs.current_tab, tabs.RosterInfoTab): + if not isinstance(self.tabs.current_tab, RosterInfoTab): self.information_win.refresh() else: info = self.tabs.current_tab.information_win @@ -1514,7 +1632,7 @@ class Core: Scroll the information buffer down """ self.information_win.scroll_down(self.information_win.height) - if not isinstance(self.tabs.current_tab, tabs.RosterInfoTab): + if not isinstance(self.tabs.current_tab, RosterInfoTab): self.information_win.refresh() else: info = self.tabs.current_tab.information_win @@ -1539,57 +1657,47 @@ class Core: """ Enable/disable the left panel. """ - enabled = config.get('enable_vertical_tab_list') + enabled = config.getbool('enable_vertical_tab_list') if not config.silent_set('enable_vertical_tab_list', str(not enabled)): self.information('Unable to write in the config file', 'Error') self.call_for_resize() - def resize_global_information_win(self): + def resize_global_information_win(self, ui_config_changed: bool = False): """ Resize the global_information_win only once at each resize. """ - if self.information_win_size > tabs.Tab.height - 6: - self.information_win_size = tabs.Tab.height - 6 - if tabs.Tab.height < 6: + if self.information_win_size > Tab.height - 6: + self.information_win_size = Tab.height - 6 + if 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) + height = (Tab.height - 1 - self.information_win_size - + Tab.tab_win_height()) + self.information_win.resize(self.information_win_size, Tab.width, + height, 0, self.information_buffer, + force=ui_config_changed) def resize_global_info_bar(self): """ Resize the GlobalInfoBar only once at each resize """ height, width = self.stdscr.getmaxyx() - if config.get('enable_vertical_tab_list'): + if config.getbool('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) + height, config.getint('vertical_tab_list_size'), 0, 0) except: log.error('Curses error on infobar resize', exc_info=True) return self.left_tab_win = windows.VerticalGlobalInfoBar( self, truncated_win) elif not self.size.core_degrade_y: - self.tab_win.resize(1, tabs.Tab.width, tabs.Tab.height - 2, 0) + self.tab_win.resize(1, Tab.width, Tab.height - 2, 0) self.left_tab_win = None - def add_message_to_text_buffer(self, buff, txt, nickname=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') - return - buff.add_message(txt, nickname=nickname) - def full_screen_redraw(self): """ Completely erase and redraw the screen @@ -1597,7 +1705,7 @@ class Core: self.stdscr.clear() self.refresh_window() - def call_for_resize(self): + def call_for_resize(self, ui_config_changed: bool = False): """ Called when we want to resize the screen """ @@ -1605,22 +1713,27 @@ class Core: # 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 + if self.stdscr is None: + raise ValueError('No output available') height, width = self.stdscr.getmaxyx() - if (config.get('enable_vertical_tab_list') + if (config.getbool('enable_vertical_tab_list') and not self.size.core_degrade_x): try: - scr = self.stdscr.subwin(0, - config.get('vertical_tab_list_size')) + scr = self.stdscr.subwin( + 0, + config.getint('vertical_tab_list_size') + ) except: log.error('Curses error on resize', exc_info=True) return else: scr = self.stdscr - tabs.Tab.resize(scr) + Tab.initial_resize(scr) self.resize_global_info_bar() - self.resize_global_information_win() + self.resize_global_information_win(ui_config_changed) for tab in self.tabs: - if config.get('lazy_resize'): + tab.ui_config_changed = True + if config.getbool('lazy_resize'): tab.need_resize = True else: tab.resize() @@ -1663,330 +1776,10 @@ class Core: """ Register the commands when poezio starts """ - self.register_command( - 'help', - self.command.help, - usage='[command]', - shortdesc='\\_o< KOIN KOIN KOIN', - completion=self.completion.help) - self.register_command( - 'join', - self.command.join, - usage="[room_name][@server][/nick] [password]", - desc="Join the specified room. You can specify a nickname " - "after a slash (/). If no nickname is specified, you will" - " use the default_nick in the configuration file. You can" - " omit the room name: you will then join the room you\'re" - " looking at (useful if you were kicked). You can also " - "provide a room_name without specifying a server, the " - "server of the room you're currently in will be used. You" - " can also provide a password to join the room.\nExamples" - ":\n/join room@server.tld\n/join room@server.tld/John\n" - "/join room2\n/join /me_again\n/join\n/join room@server" - ".tld/my_nick password\n/join / password", - shortdesc='Join a room', - completion=self.completion.join) - self.register_command( - 'exit', - self.command.quit, - desc='Just disconnect from the server and exit poezio.', - shortdesc='Exit poezio.') - self.register_command( - 'quit', - self.command.quit, - desc='Just disconnect from the server and exit poezio.', - shortdesc='Exit poezio.') - self.register_command( - 'next', self.rotate_rooms_right, shortdesc='Go to the next room.') - self.register_command( - 'prev', - self.rotate_rooms_left, - shortdesc='Go to the previous room.') - self.register_command( - 'win', - self.command.win, - usage='<number or name>', - shortdesc='Go to the specified room', - completion=self.completion.win) - self.commands['w'] = self.commands['win'] - self.register_command( - 'move_tab', - self.command.move_tab, - usage='<source> <destination>', - desc="Insert the <source> tab at the position of " - "<destination>. This will make the following tabs shift in" - " some cases (refer to the documentation). A tab can be " - "designated by its number or by the beginning of its " - "address. You can use \".\" as a shortcut for the current " - "tab.", - shortdesc='Move a tab.', - completion=self.completion.move_tab) - self.register_command( - 'destroy_room', - self.command.destroy_room, - usage='[room JID]', - desc='Try to destroy the room [room JID], or the current' - ' tab if it is a multi-user chat and [room JID] is ' - 'not given.', - shortdesc='Destroy a room.', - completion=None) - self.register_command( - 'show', - self.command.status, - usage='<availability> [status message]', - desc="Sets your availability and (optionally) your status " - "message. The <availability> argument is one of \"available" - ", chat, away, afk, dnd, busy, xa\" and the optional " - "[status message] argument will be your status message.", - shortdesc='Change your availability.', - completion=self.completion.status) - self.commands['status'] = self.commands['show'] - self.register_command( - 'bookmark_local', - self.command.bookmark_local, - usage="[roomname][/nick] [password]", - desc="Bookmark Local: Bookmark locally the specified room " - "(you will then auto-join it on each poezio start). This" - " commands uses almost the same syntaxe as /join. Type " - "/help join for syntax examples. Note that when typing " - "\"/bookmark\" on its own, the room will be bookmarked " - "with the nickname you\'re currently using in this room " - "(instead of default_nick)", - shortdesc='Bookmark a room locally.', - completion=self.completion.bookmark_local) - self.register_command( - 'bookmark', - self.command.bookmark, - usage="[roomname][/nick] [autojoin] [password]", - desc="Bookmark: Bookmark online the specified room (you " - "will then auto-join it on each poezio start if autojoin" - " is specified and is 'true'). This commands uses almost" - " the same syntax as /join. Type /help join for syntax " - "examples. Note that when typing \"/bookmark\" alone, the" - " room will be bookmarked with the nickname you\'re " - "currently using in this room (instead of default_nick).", - shortdesc="Bookmark a room online.", - completion=self.completion.bookmark) - self.register_command( - 'accept', - self.command.command_accept, - usage='[jid]', - desc='Allow the provided JID (or the selected contact ' - 'in your roster), to see your presence.', - shortdesc='Allow a user your presence.',) - self.register_command( - 'add', - self.command.command_add, - usage='<jid>', - desc='Add the specified JID to your roster, ask them to' - ' allow you to see his presence, and allow them to' - ' see your presence.', - shortdesc='Add a user to your roster.') - self.register_command( - 'reconnect', - self.command.command_reconnect, - usage="[reconnect]", - desc='Disconnect from the remote server if you are ' - 'currently connected and then connect to it again.', - shortdesc='Disconnect and reconnect to the server.') - self.register_command( - 'set', - self.command.set, - usage="[plugin|][section] <option> [value]", - desc="Set the value of an option in your configuration file." - " You can, for example, change your default nickname by " - "doing `/set default_nick toto` or your resource with `/set" - " resource blabla`. You can also set options in specific " - "sections with `/set bindings M-i ^i` or in specific plugin" - " with `/set mpd_client| host 127.0.0.1`. `toggle` can be " - "used as a special value to toggle a boolean option.", - shortdesc="Set the value of an option", - completion=self.completion.set) - self.register_command( - 'set_default', - self.command.set_default, - usage="[section] <option>", - desc="Set the default value of an option. For example, " - "`/set_default resource` will reset the resource " - "option. You can also reset options in specific " - "sections by doing `/set_default section option`.", - shortdesc="Set the default value of an option", - completion=self.completion.set_default) - self.register_command( - 'toggle', - self.command.toggle, - usage='<option>', - desc='Shortcut for /set <option> toggle', - shortdesc='Toggle an option', - completion=self.completion.toggle) - self.register_command( - 'theme', - self.command.theme, - usage='[theme name]', - desc="Reload the theme defined in the config file. If theme" - "_name is provided, set that theme before reloading it.", - shortdesc='Load a theme', - completion=self.completion.theme) - self.register_command( - 'list', - self.command.list, - usage='[server]', - desc="Get the list of public rooms" - " on the specified server.", - shortdesc='List the rooms.', - completion=self.completion.list) - self.register_command( - 'message', - self.command.message, - usage='<jid> [optional message]', - desc="Open a conversation with the specified JID (even if it" - " is not in our roster), and send a message to it, if the " - "message is specified.", - shortdesc='Send a message', - completion=self.completion.message) - self.register_command( - 'version', - self.command.version, - usage='<jid>', - desc="Get the software version of the given JID (usually its" - " XMPP client and Operating System).", - shortdesc='Get the software version of a JID.', - completion=self.completion.version) - self.register_command( - 'server_cycle', - self.command.server_cycle, - usage='[domain] [message]', - desc='Disconnect and reconnect in all the rooms in domain.', - shortdesc='Cycle a range of rooms', - completion=self.completion.server_cycle) - self.register_command( - 'bind', - self.command.bind, - usage='<key> <equ>', - desc="Bind a key to another key or to a “command”. For " - "example \"/bind ^H KEY_UP\" makes Control + h do the" - " same same as the Up key.", - completion=self.completion.bind, - shortdesc='Bind a key to another key.') - self.register_command( - 'load', - self.command.load, - usage='<plugin> [<otherplugin> …]', - shortdesc='Load the specified plugin(s)', - completion=self.plugin_manager.completion_load) - self.register_command( - 'unload', - self.command.unload, - usage='<plugin> [<otherplugin> …]', - shortdesc='Unload the specified plugin(s)', - completion=self.plugin_manager.completion_unload) - self.register_command( - 'plugins', - self.command.plugins, - shortdesc='Show the plugins in use.') - self.register_command( - 'presence', - self.command.presence, - usage='<JID> [type] [status]', - desc="Send a directed presence to <JID> and using" - " [type] and [status] if provided.", - shortdesc='Send a directed presence.', - completion=self.completion.presence) - self.register_command( - 'rawxml', - self.command.rawxml, - usage='<xml>', - shortdesc='Send a custom xml stanza.') - self.register_command( - 'invite', - self.command.invite, - usage='<jid> <room> [reason]', - desc='Invite jid in room with reason.', - shortdesc='Invite someone in a room.', - completion=self.completion.invite) - self.register_command( - 'impromptu', - self.command.impromptu, - usage='<jid> [jid ...]', - desc='Invite specified JIDs into a newly created room.', - shortdesc='Invite specified JIDs into newly created room.', - completion=self.completion.impromptu) - self.register_command( - 'invitations', - self.command.invitations, - shortdesc='Show the pending invitations.') - self.register_command( - 'bookmarks', - self.command.bookmarks, - shortdesc='Show the current bookmarks.') - self.register_command( - 'remove_bookmark', - self.command.remove_bookmark, - usage='[jid]', - desc="Remove the specified bookmark, or the " - "bookmark on the current tab, if any.", - shortdesc='Remove a bookmark', - completion=self.completion.remove_bookmark) - self.register_command( - 'xml_tab', self.command.xml_tab, shortdesc='Open an XML tab.') - self.register_command( - 'runkey', - self.command.runkey, - usage='<key>', - shortdesc='Execute the action defined for <key>.', - completion=self.completion.runkey) - self.register_command( - 'self', self.command.self_, shortdesc='Remind you of who you are.') - self.register_command( - 'last_activity', - self.command.last_activity, - usage='<jid>', - desc='Informs you of the last activity of a JID.', - shortdesc='Get the activity of someone.', - completion=self.completion.last_activity) - self.register_command( - 'ad-hoc', - self.command.adhoc, - usage='<jid>', - shortdesc='List available ad-hoc commands on the given jid') - self.register_command( - 'reload', - self.command.reload, - shortdesc='Reload the config. You can achieve the same by ' - 'sending SIGUSR1 to poezio.') - - if config.get('enable_user_activity'): - self.register_command( - 'activity', - self.command.activity, - usage='[<general> [specific] [text]]', - desc='Send your current activity to your contacts ' - '(use the completion). Nothing means ' - '"stop broadcasting an activity".', - shortdesc='Send your activity.', - completion=self.completion.activity) - if config.get('enable_user_mood'): - self.register_command( - 'mood', - self.command.mood, - usage='[<mood> [text]]', - desc='Send your current mood to your contacts ' - '(use the completion). Nothing means ' - '"stop broadcasting a mood".', - shortdesc='Send your mood.', - completion=self.completion.mood) - if config.get('enable_user_gaming'): - self.register_command( - 'gaming', - self.command.gaming, - usage='[<game name> [server address]]', - desc='Send your current gaming activity to ' - 'your contacts. Nothing means "stop ' - 'broadcasting a gaming activity".', - shortdesc='Send your gaming activity.', - completion=None) - - def check_blocking(self, features): + for command in get_commands(self.command, self.completion, self.plugin_manager): + self.register_command(**command) + + def check_blocking(self, features: List[str]): if 'urn:xmpp:blocking' in features and not self.xmpp.anon: self.register_command( 'block', @@ -2005,12 +1798,12 @@ class Core: ####################### Random things to move ################################# - def join_initial_rooms(self, bookmarks): + def join_initial_rooms(self, bookmarks: List[Bookmark]): """Join all rooms given in the iterator `bookmarks`""" for bm in bookmarks: - if not (bm.autojoin or config.get('open_all_bookmarks')): + if not (bm.autojoin or config.getbool('open_all_bookmarks')): continue - tab = self.tabs.by_name_and_class(bm.jid, tabs.MucTab) + tab = self.tabs.by_name_and_class(bm.jid, MucTab) nick = bm.nick if bm.nick else self.own_nick if not tab: tab = self.open_new_room( @@ -2018,28 +1811,21 @@ class Core: self.initial_joins.append(bm.jid) # 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, - status=self.status.message, - show=self.status.show, - tab=tab) - if tab._text_buffer.last_message is None: - asyncio.ensure_future(mam.on_tab_open(tab)) - - def check_bookmark_storage(self, features): + if bm.autojoin and tab: + tab.join() + + async def check_bookmark_storage(self, features: List[str]): private = 'jabber:iq:private' in features pep_ = 'http://jabber.org/protocol/pubsub#publish' in features self.bookmarks.available_storage['private'] = private self.bookmarks.available_storage['pep'] = pep_ - def _join_remote_only(iq): - if iq['type'] == 'error': - type_ = iq['error']['type'] - condition = iq['error']['condition'] + if not self.xmpp.anon and config.getbool('use_remote_bookmarks'): + try: + await self.bookmarks.get_remote(self.xmpp, self.information) + except IqError as error: + type_ = error.iq['error']['type'] + condition = error.iq['error']['condition'] if not (type_ == 'cancel' and condition == 'item-not-found'): self.information( 'Unable to fetch the remote' @@ -2048,38 +1834,37 @@ class Core: remote_bookmarks = self.bookmarks.remote() self.join_initial_rooms(remote_bookmarks) - if not self.xmpp.anon and config.get('use_remote_bookmarks'): - self.bookmarks.get_remote(self.xmpp, self.information, - _join_remote_only) - - def room_error(self, error, room_name): + def room_error(self, error, room_name: str) -> None: """ Display the error in the tab """ - tab = self.tabs.by_name_and_class(room_name, tabs.MucTab) + tab = self.tabs.by_name_and_class(room_name, MucTab) if not tab: return - error_message = self.get_error_message(error) + error_message = get_error_message(error) tab.add_message( - error_message, - highlight=True, - nickname='Error', - nick_color=get_theme().COLOR_ERROR_MSG, - typ=2) + UIMessage( + error_message, + level='Error', + ), + ) 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)' - tab.add_message(msg, typ=2) + tab.add_message(PersistentInfoMessage(msg)) if code == '409': - if config.get('alternative_nickname') != '': + if config.getstr('alternative_nickname') != '': if not tab.joined: - tab.own_nick += config.get('alternative_nickname') + tab.own_nick += config.getstr('alternative_nickname') tab.join() else: if not tab.joined: tab.add_message( - 'You can join the room with an other nick, by typing "/join /other_nick"', - typ=2) + PersistentInfoMessage( + 'You can join the room with another nick, ' + 'by typing "/join /other_nick"' + ) + ) self.refresh_window() @@ -2088,13 +1873,18 @@ class KeyDict(dict): A dict, with a wrapper for get() that will return a custom value if the key starts with _exc_ """ + try_execute: Optional[Callable[[str], Any]] - def get(self, key: str, default: Optional[Callable] = None) -> Callable: + def get(self, key: str, default=None) -> Callable: if isinstance(key, str) and key.startswith('_exc_') and len(key) > 5: - return lambda: dict.get(self, '_exc_')(key[5:]) + if self.try_execute is not None: + try_execute = self.try_execute + return lambda: try_execute(key[5:]) + raise ValueError("KeyDict not initialized") return dict.get(self, key, default) + def replace_key_with_bound(key: str) -> str: """ Replace an inputted key with the one defined as its replacement |