summaryrefslogtreecommitdiff
path: root/poezio/core/core.py
diff options
context:
space:
mode:
Diffstat (limited to 'poezio/core/core.py')
-rw-r--r--poezio/core/core.py1010
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