diff options
Diffstat (limited to 'poezio/tabs/basetabs.py')
-rw-r--r-- | poezio/tabs/basetabs.py | 549 |
1 files changed, 396 insertions, 153 deletions
diff --git a/poezio/tabs/basetabs.py b/poezio/tabs/basetabs.py index 578668fc..793eae62 100644 --- a/poezio/tabs/basetabs.py +++ b/poezio/tabs/basetabs.py @@ -13,26 +13,57 @@ This module also defines ChatTabs, the parent class for all tabs revolving around chats. """ +from __future__ import annotations + import logging import string -import time +import asyncio +from copy import copy +from math import ceil, log10 from datetime import datetime -from xml.etree import cElementTree as ET -from typing import Any, Callable, Dict, List, Optional - -from slixmpp import JID, Message - +from xml.etree import ElementTree as ET +from xml.sax import SAXParseException +from typing import ( + Any, + Callable, + cast, + Dict, + List, + Optional, + Union, + Tuple, + TYPE_CHECKING, +) + +from poezio import ( + poopt, + timed_events, + xhtml, + windows +) from poezio.core.structs import Command, Completion, Status -from poezio import timed_events -from poezio import windows -from poezio import xhtml -from poezio.common import safeJID from poezio.config import config -from poezio.decorators import refresh_wrapper +from poezio.decorators import command_args_parser, refresh_wrapper from poezio.logger import logger +from poezio.log_loader import MAMFiller, LogLoader from poezio.text_buffer import TextBuffer from poezio.theming import get_theme, dump_tuple -from poezio.decorators import command_args_parser +from poezio.user import User +from poezio.ui.funcs import truncate_nick +from poezio.timed_events import DelayedEvent +from poezio.ui.types import ( + BaseMessage, + Message, + PersistentInfoMessage, + LoggableTrait, +) + +from slixmpp import JID, InvalidJID, Message as SMessage + +if TYPE_CHECKING: + from _curses import _CursesWindow # pylint: disable=E0611 + from poezio.size_manager import SizeManager + from poezio.core.core import Core log = logging.getLogger(__name__) @@ -90,29 +121,42 @@ SHOW_NAME = { class Tab: - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} # Placeholder values, set on resize - height = 1 - width = 1 - - def __init__(self, core): + height: int = 1 + width: int = 1 + core: Core + input: Optional[windows.Input] + key_func: Dict[str, Callable[[], Any]] + commands: Dict[str, Command] + need_resize: bool + ui_config_changed: bool + + def __init__(self, core: Core): self.core = core self.nb = 0 - if not hasattr(self, 'name'): - self.name = self.__class__.__name__ + if not hasattr(self, '_name'): + self._name = self.__class__.__name__ self.input = None self.closed = False self._state = 'normal' self._prev_state = None self.need_resize = False + self.ui_config_changed = False self.key_func = {} # each tab should add their keys in there # and use them in on_input self.commands = {} # and their own commands @property - def size(self) -> int: + def name(self) -> str: + if hasattr(self, '_name'): + return self._name + return '' + + @property + def size(self) -> SizeManager: return self.core.size @staticmethod @@ -121,23 +165,27 @@ class Tab: Returns 1 or 0, depending on if we are using the vertical tab list or not. """ - if config.get('enable_vertical_tab_list'): + if config.getbool('enable_vertical_tab_list'): return 0 return 1 @property - def info_win(self): + def info_win(self) -> windows.TextWin: return self.core.information_win @property - def color(self): + def color(self) -> Union[Tuple[int, int], Tuple[int, int, 'str']]: return STATE_COLORS[self._state]() @property - def vertical_color(self): + def vertical_color(self) -> Union[Tuple[int, int], Tuple[int, int, 'str']]: return VERTICAL_STATE_COLORS[self._state]() @property + def priority(self) -> Union[int, float]: + return STATE_PRIORITY.get(self._state, -1) + + @property def state(self) -> str: return self._state @@ -175,7 +223,7 @@ class Tab: self._state = 'normal' @staticmethod - def resize(scr): + def initial_resize(scr: _CursesWindow): Tab.height, Tab.width = scr.getmaxyx() windows.base_wins.TAB_WIN = scr @@ -212,7 +260,7 @@ class Tab: *, desc='', shortdesc='', - completion: Optional[Callable] = None, + completion: Optional[Callable[[windows.Input], Completion]] = None, usage=''): """ Add a command @@ -241,7 +289,7 @@ class Tab: ['/%s' % (name) for name in sorted(self.commands)] the_input.new_completion(words, 0) # Do not try to cycle command completion if there was only - # one possibily. The next tab will complete the argument. + # one possibility. The next tab will complete the argument. # Otherwise we would need to add a useless space before being # able to complete the arguments. hit_copy = set(the_input.hit_list) @@ -264,7 +312,6 @@ class Tab: comp = command.comp(the_input) if comp: return comp.run() - return comp return False def execute_command(self, provided_text: str) -> bool: @@ -272,8 +319,10 @@ class Tab: Execute the command in the input and return False if the input didn't contain a command """ + if self.input is None: + raise NotImplementedError txt = provided_text or self.input.key_enter() - if txt.startswith('/') and not txt.startswith('//') and\ + if txt and txt.startswith('/') and not txt.startswith('//') and\ not txt.startswith('/me '): command = txt.strip().split()[0][1:] arg = txt[2 + len(command):] # jump the '/' and the ' ' @@ -301,13 +350,16 @@ class Tab: if func: if hasattr(self.input, "reset_completion"): self.input.reset_completion() - func(arg) + if asyncio.iscoroutinefunction(func): + asyncio.create_task(func(arg)) + else: + func(arg) return True else: return False - def refresh_tab_win(self): - if config.get('enable_vertical_tab_list'): + def refresh_tab_win(self) -> None: + if config.getbool('enable_vertical_tab_list'): left_tab_win = self.core.left_tab_win if left_tab_win and not self.size.core_degrade_x: left_tab_win.refresh() @@ -338,24 +390,18 @@ class Tab: """ return self.name - def get_text_window(self) -> Optional[windows.TextWin]: - """ - Returns the principal TextWin window, if there's one - """ - return None - def on_input(self, key: str, raw: bool): """ raw indicates if the key should activate the associated command or not. """ pass - def update_commands(self): + def update_commands(self) -> None: for c in self.plugin_commands: if c not in self.commands: self.commands[c] = self.plugin_commands[c] - def update_keys(self): + def update_keys(self) -> None: for k in self.plugin_keys: if k not in self.key_func: self.key_func[k] = self.plugin_keys[k] @@ -414,7 +460,7 @@ class Tab: """ pass - def on_close(self): + def on_close(self) -> None: """ Called when the tab is to be closed """ @@ -422,7 +468,7 @@ class Tab: self.input.on_delete() self.closed = True - def matching_names(self) -> List[str]: + def matching_names(self) -> List[Tuple[int, str]]: """ Returns a list of strings that are used to name a tab with the /win command. For example you could switch to a tab that returns @@ -436,6 +482,9 @@ class Tab: class GapTab(Tab): + def __init__(self): + return + def __bool__(self): return False @@ -443,7 +492,7 @@ class GapTab(Tab): return 0 @property - def name(self): + def name(self) -> str: return '' def refresh(self): @@ -458,23 +507,37 @@ class ChatTab(Tab): Also, ^M is already bound to on_enter And also, add the /say command """ - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} + last_sent_message: Optional[SMessage] message_type = 'chat' + timed_event_paused: Optional[DelayedEvent] + timed_event_not_paused: Optional[DelayedEvent] + mam_filler: Optional[MAMFiller] + e2e_encryption: Optional[str] = None - def __init__(self, core, jid=''): + def __init__(self, core, jid: Union[JID, str]): Tab.__init__(self, core) - self.name = jid - self.text_win = None + + if not isinstance(jid, JID): + jid = JID(jid) + assert jid.domain + self._jid = jid + #: Is the tab currently requesting MAM data? + self.query_status = False + self._name = jid.full + self.text_win = windows.TextWin() self.directed_presence = None self._text_buffer = TextBuffer() + self._text_buffer.add_window(self.text_win) + self.mam_filler = None self.chatstate = None # can be "active", "composing", "paused", "gone", "inactive" # We keep a reference of the event that will set our chatstate to "paused", so that # we can delete it or change it if we need to self.timed_event_paused = None self.timed_event_not_paused = None # Keeps the last sent message to complete it easily in completion_correct, and to replace it. - self.last_sent_message = {} + self.last_sent_message = None self.key_func['M-v'] = self.move_separator self.key_func['M-h'] = self.scroll_separator self.key_func['M-/'] = self.last_words_completion @@ -485,6 +548,12 @@ class ChatTab(Tab): usage='<message>', shortdesc='Send the message.') self.register_command( + 'scrollback', + self.command_scrollback, + usage="end home clear status goto <+|-linecount>|<linenum>|<timestamp>", + shortdesc='Scrollback to the given line number, message, or clear the buffer.') + self.commands['sb'] = self.commands['scrollback'] + self.register_command( 'xhtml', self.command_xhtml, usage='<custom xhtml>', @@ -497,73 +566,79 @@ class ChatTab(Tab): desc='Fix the last message with whatever you want.', shortdesc='Correct the last message.', completion=self.completion_correct) - self.chat_state = None + self.chat_state: Optional[str] = None self.update_commands() self.update_keys() - # Get the logs - log_nb = config.get('load_log') - logs = self.load_logs(log_nb) + @property + def name(self) -> str: + if self._name is not None: + return self._name + return self._jid.full + + @name.setter + def name(self, value: Union[JID, str]) -> None: + if isinstance(value, JID): + self.jid = value + elif isinstance(value, str): + try: + value = JID(value) + if value.domain: + self._jid = value + except InvalidJID: + self._name = str(value) + else: + raise TypeError("Name %r must be of type JID or str." % value) - if logs: - for message in logs: - self._text_buffer.add_message(**message) + @property + def log_name(self) -> str: + """Name used for the log filename""" + return self.jid.bare @property - def general_jid(self) -> JID: - return NotImplementedError + def jid(self) -> JID: + return copy(self._jid) - def load_logs(self, log_nb: int) -> Optional[List[Dict[str, Any]]]: - logs = logger.get_logs(safeJID(self.name).bare, log_nb) - return logs + @jid.setter + def jid(self, value: JID) -> None: + if not isinstance(value, JID): + raise TypeError("Jid %r must be of type JID." % value) + assert value.domain + self._jid = value + + @property + def general_jid(self) -> JID: + raise NotImplementedError - def log_message(self, - txt: str, - nickname: str, - time: Optional[datetime] = None, - typ=1): + def log_message(self, message: BaseMessage): """ Log the messages in the archives. """ - name = safeJID(self.name).bare - if not logger.log_message(name, nickname, txt, date=time, typ=typ): + if not isinstance(message, LoggableTrait): + return + if not logger.log_message(self.log_name, message): self.core.information('Unable to write in the log file', 'Error') - def add_message(self, - txt, - time=None, - nickname=None, - forced_user=None, - nick_color=None, - identifier=None, - jid=None, - history=None, - typ=1, - highlight=False): - self.log_message(txt, nickname, time=time, typ=typ) - self._text_buffer.add_message( - txt, - time=time, - nickname=nickname, - highlight=highlight, - nick_color=nick_color, - history=history, - user=forced_user, - identifier=identifier, - jid=jid) + def add_message(self, message: BaseMessage): + self.log_message(message) + self._text_buffer.add_message(message) def modify_message(self, - txt, - old_id, - new_id, - user=None, - jid=None, - nickname=None): - self.log_message(txt, nickname, typ=1) + txt: str, + old_id: str, + new_id: str, + time: Optional[datetime], + delayed: bool = False, + nickname: Optional[str] = None, + user: Optional[User] = None, + jid: Optional[JID] = None, + ) -> bool: message = self._text_buffer.modify_message( - txt, old_id, new_id, time=time, user=user, jid=jid) + txt, old_id, new_id, user=user, jid=jid, time=time + ) if message: - self.text_win.modify_message(old_id, message) + self.log_message(message) + self.text_win.modify_message(message.identifier, message) self.core.refresh_window() return True return False @@ -584,16 +659,20 @@ class ChatTab(Tab): for word in txt.split(): if len(word) >= 4 and word not in words: words.append(word) - words.extend([word for word in config.get('words').split(':') if word]) + words.extend([word for word in config.getlist('words') if word]) self.input.auto_completion(words, ' ', quotify=False) def on_enter(self): + if self.input is None: + raise NotImplementedError txt = self.input.key_enter() if txt: if not self.execute_command(txt): if txt.startswith('//'): txt = txt[1:] - self.command_say(xhtml.convert_simple_to_full_colors(txt)) + asyncio.ensure_future( + self.command_say(xhtml.convert_simple_to_full_colors(txt)) + ) self.cancel_paused_delay() @command_args_parser.raw @@ -605,26 +684,26 @@ class ChatTab(Tab): if message: message.send() - def generate_xhtml_message(self, arg: str) -> Message: + def generate_xhtml_message(self, arg: str) -> Optional[SMessage]: if not arg: - return + return None try: body = xhtml.clean_text( xhtml.xhtml_to_poezio_colors(arg, force=True)) ET.fromstring(arg) - except: + except SAXParseException: self.core.information('Could not send custom xhtml', 'Error') - log.error('/xhtml: Unable to send custom xhtml', exc_info=True) - return + log.error('/xhtml: Unable to send custom xhtml') + return None - msg = self.core.xmpp.make_message(self.get_dest_jid()) + msg: SMessage = self.core.xmpp.make_message(self.get_dest_jid()) msg['body'] = body msg.enable('html') msg['html']['body'] = arg return msg def get_dest_jid(self) -> JID: - return self.name + return self.jid @refresh_wrapper.always def command_clear(self, ignored): @@ -634,27 +713,31 @@ class ChatTab(Tab): self._text_buffer.messages = [] self.text_win.rebuild_everything(self._text_buffer) - def check_send_chat_state(self): + def check_send_chat_state(self) -> bool: "If we should send a chat state" return True - def send_chat_state(self, state, always_send=False): + def send_chat_state(self, state: str, always_send: bool = False) -> None: """ Send an empty chatstate message """ + from poezio.tabs import PrivateTab + if self.check_send_chat_state(): if state in ('active', 'inactive', 'gone') and self.inactive and not always_send: return if config.get_by_tabname('send_chat_states', self.general_jid): - msg = self.core.xmpp.make_message(self.get_dest_jid()) + msg: SMessage = self.core.xmpp.make_message(self.get_dest_jid()) msg['type'] = self.message_type msg['chat_state'] = state self.chat_state = state + msg['no-store'] = True + if isinstance(self, PrivateTab): + msg.enable('muc') msg.send() - return True - def send_composing_chat_state(self, empty_after): + def send_composing_chat_state(self, empty_after: bool) -> None: """ Send the "active" or "composing" chatstate, depending on the the current status of the input @@ -690,7 +773,7 @@ class ChatTab(Tab): self.core.add_timed_event(new_event) self.timed_event_not_paused = new_event - def cancel_paused_delay(self): + def cancel_paused_delay(self) -> None: """ Remove that event from the list and set it to None. Called for example when the input is emptied, or when the message @@ -699,11 +782,22 @@ class ChatTab(Tab): if self.timed_event_paused is not None: self.core.remove_timed_event(self.timed_event_paused) self.timed_event_paused = None - self.core.remove_timed_event(self.timed_event_not_paused) - self.timed_event_not_paused = None + if self.timed_event_not_paused is not None: + self.core.remove_timed_event(self.timed_event_not_paused) + self.timed_event_not_paused = None + + def set_last_sent_message(self, msg: SMessage, correct: bool = False) -> None: + """Ensure last_sent_message is set with the correct attributes""" + if correct: + # XXX: Is the copy needed. Is the object passed here reused + # afterwards? Who knows. + msg = cast(SMessage, copy(msg)) + if self.last_sent_message is not None: + msg['id'] = self.last_sent_message['id'] + self.last_sent_message = msg @command_args_parser.raw - def command_correct(self, line): + async def command_correct(self, line: str) -> None: """ /correct <fixed message> """ @@ -713,7 +807,7 @@ class ChatTab(Tab): if not self.last_sent_message: self.core.information('There is no message to correct.', 'Error') return - self.command_say(line, correct=True) + await self.command_say(line, correct=True) def completion_correct(self, the_input): if self.last_sent_message and the_input.get_argument_position() == 1: @@ -726,26 +820,153 @@ class ChatTab(Tab): @property def inactive(self) -> bool: """Whether we should send inactive or active as a chatstate""" - return self.core.status.show in ('xa', 'away') or\ - (hasattr(self, 'directed_presence') and not self.directed_presence) + return self.core.status.show in ('xa', 'away') or ( + hasattr(self, 'directed_presence') + and self.directed_presence is not None + and self.directed_presence + ) - def move_separator(self): + def move_separator(self) -> None: self.text_win.remove_line_separator() self.text_win.add_line_separator(self._text_buffer) self.text_win.refresh() - self.input.refresh() + if self.input: + self.input.refresh() def get_conversation_messages(self): return self._text_buffer.messages - def check_scrolled(self): + def check_scrolled(self) -> None: if self.text_win.pos != 0: self.state = 'scrolled' @command_args_parser.raw - def command_say(self, line, correct=False): + async def command_say(self, line: str, attention: bool = False, correct: bool = False): pass + def goto_build_lines(self, new_date): + text_buffer = self._text_buffer + built_lines = [] + message_count = 0 + timestamp = config.getbool('show_timestamps') + nick_size = config.getint('max_nick_length') + theme = get_theme() + for message in text_buffer.messages: + # Build lines of a message + txt = message.txt + nick = truncate_nick(message.nickname, nick_size) + offset = 0 + theme = get_theme() + if message.ack: + if message.ack > 0: + offset += poopt.wcswidth(theme.CHAR_ACK_RECEIVED) + 1 + else: + offset += poopt.wcswidth(theme.CHAR_NACK) + 1 + if nick: + offset += poopt.wcswidth(nick) + 2 + if message.revisions > 0: + offset += ceil(log10(message.revisions + 1)) + if message.me: + offset += 1 + if timestamp: + if message.history: + offset += 1 + theme.LONG_TIME_FORMAT_LENGTH + lines = poopt.cut_text(txt, self.text_win.width - offset - 1) + for line in lines: + built_lines.append(line) + # Find the message with timestamp less than or equal to the queried + # timestamp and goto that location in the tab. + if message.time <= new_date: + message_count += 1 + if len(self.text_win.built_lines) - self.text_win.height >= len(built_lines): + self.text_win.pos = len(self.text_win.built_lines) - self.text_win.height - len(built_lines) + 1 + else: + self.text_win.pos = 0 + if message_count == 0: + self.text_win.scroll_up(len(self.text_win.built_lines)) + self.core.refresh_window() + + @command_args_parser.quoted(0, 2) + def command_scrollback(self, args): + """ + /sb clear + /sb home + /sb end + /sb goto <+|-linecount>|<linenum>|<timestamp> + The format of timestamp must be ‘[dd[.mm]-<days ago>] hh:mi[:ss]’ + """ + if args is None or len(args) == 0: + args = ['end'] + if len(args) == 1: + if args[0] == 'end': + self.text_win.scroll_down(len(self.text_win.built_lines)) + self.core.refresh_window() + return + elif args[0] == 'home': + self.text_win.scroll_up(len(self.text_win.built_lines)) + self.core.refresh_window() + return + elif args[0] == 'clear': + self._text_buffer.messages = [] + self.text_win.rebuild_everything(self._text_buffer) + self.core.refresh_window() + return + elif args[0] == 'status': + self.core.information('Total %s lines in this tab.' % len(self.text_win.built_lines), 'Info') + return + elif len(args) == 2 and args[0] == 'goto': + for fmt in ('%d %H:%M', '%d %H:%M:%S', '%d:%m %H:%M', '%d:%m %H:%M:%S', '%H:%M', '%H:%M:%S'): + try: + new_date = datetime.strptime(args[1], fmt) + if 'd' in fmt and 'm' in fmt: + new_date = new_date.replace(year=datetime.now().year) + elif 'd' in fmt: + new_date = new_date.replace(year=datetime.now().year, month=datetime.now().month) + else: + new_date = new_date.replace(year=datetime.now().year, month=datetime.now().month, day=datetime.now().day) + except ValueError: + pass + if args[1].startswith('-'): + # Check if the user is giving argument of type goto <-linecount> or goto [-<days ago>] hh:mi[:ss] + if ' ' in args[1]: + new_args = args[1].split(' ') + new_args[0] = new_args[0].strip('-') + new_date = datetime.now() + if new_args[0].isdigit(): + new_date = new_date.replace(day=new_date.day - int(new_args[0])) + for fmt in ('%H:%M', '%H:%M:%S'): + try: + arg_date = datetime.strptime(new_args[1], fmt) + new_date = new_date.replace(hour=arg_date.hour, minute=arg_date.minute, second=arg_date.second) + except ValueError: + pass + else: + scroll_len = args[1].strip('-') + if scroll_len.isdigit(): + self.text_win.scroll_down(int(scroll_len)) + self.core.refresh_window() + return + elif args[1].startswith('+'): + scroll_len = args[1].strip('+') + if scroll_len.isdigit(): + self.text_win.scroll_up(int(scroll_len)) + self.core.refresh_window() + return + # Check for the argument of type goto <linenum> + elif args[1].isdigit(): + if len(self.text_win.built_lines) - self.text_win.height >= int(args[1]): + self.text_win.pos = len(self.text_win.built_lines) - self.text_win.height - int(args[1]) + self.core.refresh_window() + return + else: + self.text_win.pos = 0 + self.core.refresh_window() + return + elif args[1] == '0': + args = ['home'] + # new_date is the timestamp for which the user has queried. + self.goto_build_lines(new_date) + def on_line_up(self): return self.text_win.scroll_up(1) @@ -753,6 +974,11 @@ class ChatTab(Tab): return self.text_win.scroll_down(1) def on_scroll_up(self): + if not self.query_status: + from poezio.log_loader import LogLoader + asyncio.create_task( + LogLoader(logger, self, config.getbool('use_log')).scroll_requested() + ) return self.text_win.scroll_up(self.text_win.height - 1) def on_scroll_down(self): @@ -770,15 +996,15 @@ class ChatTab(Tab): class OneToOneTab(ChatTab): - def __init__(self, core, jid=''): + def __init__(self, core, jid, initial=None): ChatTab.__init__(self, core, jid) self.__status = Status("", "") self.last_remote_message = datetime.now() + self._initial_log = asyncio.Event() # Set to true once the first disco is done self.__initial_disco = False - self.check_features() self.register_command( 'unquery', self.command_unquery, shortdesc='Close the tab.') self.register_command( @@ -790,6 +1016,30 @@ class OneToOneTab(ChatTab): shortdesc='Request the attention.', desc='Attention: Request the attention of the contact. Can also ' 'send a message along with the attention.') + asyncio.create_task(self.init_logs(initial=initial)) + + async def init_logs(self, initial: Optional[SMessage] = None) -> None: + use_log = config.get_by_tabname('use_log', self.jid) + mam_sync = config.get_by_tabname('mam_sync', self.jid) + if use_log and mam_sync: + limit = config.get_by_tabname('mam_sync_limit', self.jid) + mam_filler = MAMFiller(logger, self, limit) + self.mam_filler = mam_filler + + if initial is not None: + # If there is an initial message, throw it back into the + # text buffer if it cannot be fetched from mam + await mam_filler.done.wait() + if mam_filler.result == 0: + await self.handle_message(initial) + elif use_log and initial: + await self.handle_message(initial, display=False) + elif initial: + await self.handle_message(initial) + await LogLoader(logger, self, use_log, self._initial_log).tab_open() + + async def handle_message(self, msg: SMessage, display: bool = True): + pass def remote_user_color(self): return dump_tuple(get_theme().COLOR_REMOTE_USER) @@ -801,7 +1051,7 @@ class OneToOneTab(ChatTab): return self.__status = status hide_status_change = config.get_by_tabname('hide_status_change', - safeJID(self.name).bare) + self.jid.bare) now = datetime.now() dff = now - self.last_remote_message if hide_status_change > -1 and dff.total_seconds() > hide_status_change: @@ -816,9 +1066,11 @@ class OneToOneTab(ChatTab): msg += 'status: %s, ' % status.message if status.show in SHOW_NAME: msg += 'show: %s, ' % SHOW_NAME[status.show] - self.add_message(msg[:-2], typ=2) + self.add_message( + PersistentInfoMessage(txt=msg[:-2]) + ) - def ack_message(self, msg_id, msg_jid): + def ack_message(self, msg_id: str, msg_jid: JID): """ Ack a message """ @@ -827,9 +1079,9 @@ class OneToOneTab(ChatTab): self.text_win.modify_message(msg_id, new_msg) self.core.refresh_window() - def nack_message(self, error, msg_id, msg_jid): + def nack_message(self, error: str, msg_id: str, msg_jid: JID): """ - Ack a message + Non-ack a message (e.g. timeout) """ new_msg = self._text_buffer.nack_message(error, msg_id, msg_jid) if new_msg: @@ -848,26 +1100,21 @@ class OneToOneTab(ChatTab): message.send() body = xhtml.xhtml_to_poezio_colors(xhtml_data, force=True) self._text_buffer.add_message( - body, - nickname=self.core.own_nick, - nick_color=get_theme().COLOR_OWN_NICK, - identifier=message['id'], - jid=self.core.xmpp.boundjid) + Message( + body, + nickname=self.core.own_nick, + nick_color=get_theme().COLOR_OWN_NICK, + identifier=message['id'], + jid=self.core.xmpp.boundjid, + ) + ) self.refresh() - def check_features(self): - "check the features supported by the other party" - if safeJID(self.get_dest_jid()).resource: - self.core.xmpp.plugin['xep_0030'].get_info( - jid=self.get_dest_jid(), - timeout=5, - callback=self.features_checked) - @command_args_parser.raw - def command_attention(self, message): + async def command_attention(self, message): """/attention [message]""" - if message is not '': - self.command_say(message, attention=True) + if message != '': + await self.command_say(message, attention=True) else: msg = self.core.xmpp.make_message(self.get_dest_jid()) msg['type'] = 'chat' @@ -875,7 +1122,7 @@ class OneToOneTab(ChatTab): msg.send() @command_args_parser.raw - def command_say(self, line, correct=False, attention=False): + async def command_say(self, line: str, attention: bool = False, correct: bool = False): pass @command_args_parser.ignored @@ -899,7 +1146,3 @@ class OneToOneTab(ChatTab): msg = msg % (self.name, feature, command_name) self.core.information(msg, 'Info') return True - - def features_checked(self, iq): - "Features check callback" - features = iq['disco_info'].get_features() or [] |