diff options
Diffstat (limited to 'poezio/tabs/privatetab.py')
-rw-r--r-- | poezio/tabs/privatetab.py | 338 |
1 files changed, 195 insertions, 143 deletions
diff --git a/poezio/tabs/privatetab.py b/poezio/tabs/privatetab.py index 8f5f4d6f..1909e3c1 100644 --- a/poezio/tabs/privatetab.py +++ b/poezio/tabs/privatetab.py @@ -10,40 +10,46 @@ both participant’s nicks. It also has slightly different features than the ConversationTab (such as tab-completion on nicks from the room). """ +import asyncio import curses import logging +from datetime import datetime from typing import Dict, Callable +from slixmpp import JID +from slixmpp.stanza import Message as SMessage + from poezio.tabs import OneToOneTab, MucTab, Tab +from poezio import common from poezio import windows from poezio import xhtml -from poezio.common import safeJID -from poezio.config import config +from poezio.config import config, get_image_cache from poezio.core.structs import Command from poezio.decorators import refresh_wrapper -from poezio.logger import logger from poezio.theming import get_theme, dump_tuple from poezio.decorators import command_args_parser +from poezio.text_buffer import CorrectionError +from poezio.ui.types import ( + Message, + PersistentInfoMessage, +) log = logging.getLogger(__name__) class PrivateTab(OneToOneTab): """ - The tab containg a private conversation (someone from a MUC) + The tab containing a private conversation (someone from a MUC) """ - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} message_type = 'chat' - additional_information = {} # type: Dict[str, Callable[[str], str]] + additional_information: Dict[str, Callable[[str], str]] = {} - def __init__(self, core, name, nick): - OneToOneTab.__init__(self, core, name) + def __init__(self, core, jid, nick, initial=None): + OneToOneTab.__init__(self, core, jid, initial) self.own_nick = nick - self.name = name - self.text_win = windows.TextWin() - self._text_buffer.add_window(self.text_win) self.info_header = windows.PrivateInfoWin() self.input = windows.MessageInput() # keys @@ -53,7 +59,7 @@ class PrivateTab(OneToOneTab): 'info', self.command_info, desc= - 'Display some information about the user in the MUC: its/his/her role, affiliation, status and status message.', + 'Display some information about the user in the MUC: their role, affiliation, status and status message.', shortdesc='Info about the user.') self.register_command( 'version', @@ -62,30 +68,41 @@ class PrivateTab(OneToOneTab): 'Get the software version of the current interlocutor (usually its XMPP client and Operating System).', shortdesc='Get the software version of a jid.') self.resize() - self.parent_muc = self.core.tabs.by_name_and_class( - safeJID(name).bare, MucTab) + self.parent_muc = self.core.tabs.by_name_and_class(self.jid.bare, MucTab) self.on = True self.update_commands() self.update_keys() + @property + def log_name(self) -> str: + """Overriden from ChatTab because this is a case where we want the full JID""" + return self.jid.full + def remote_user_color(self): - user = self.parent_muc.get_user_by_name(safeJID(self.name).resource) + user = self.parent_muc.get_user_by_name(self.jid.resource) if user: return dump_tuple(user.color) return super().remote_user_color() @property - def general_jid(self): - return self.name + def general_jid(self) -> JID: + return self.jid - def get_dest_jid(self): - return self.name + def get_dest_jid(self) -> JID: + return self.jid @property - def nick(self): + def nick(self) -> str: return self.get_nick() + def ack_message(self, msg_id: str, msg_jid: JID): + if JID(msg_jid).bare == self.core.xmpp.boundjid.bare: + msg_jid = JID(self.jid.bare) + msg_jid.resource = self.own_nick + super().ack_message(msg_id, msg_jid) + @staticmethod + @refresh_wrapper.always def add_information_element(plugin_name, callback): """ Lets a plugin add its own information to the PrivateInfoWin @@ -93,22 +110,10 @@ class PrivateTab(OneToOneTab): PrivateTab.additional_information[plugin_name] = callback @staticmethod + @refresh_wrapper.always def remove_information_element(plugin_name): del PrivateTab.additional_information[plugin_name] - def load_logs(self, log_nb): - logs = logger.get_logs( - safeJID(self.name).full.replace('/', '\\'), log_nb) - return logs - - def log_message(self, txt, nickname, time=None, typ=1): - """ - Log the messages in the archives. - """ - if not logger.log_message( - self.name, nickname, txt, date=time, typ=typ): - self.core.information('Unable to write in the log file', 'Error') - def on_close(self): super().on_close() self.parent_muc.privates.remove(self) @@ -124,7 +129,7 @@ class PrivateTab(OneToOneTab): compare_users = lambda x: x.last_talked word_list = [user.nick for user in sorted(self.parent_muc.users, key=compare_users, reverse=True)\ if user.nick != self.own_nick] - after = config.get('after_completion') + ' ' + after = config.getstr('after_completion') + ' ' input_pos = self.input.pos if ' ' not in self.input.get_text()[:input_pos] or (self.input.last_completion and\ self.input.get_text()[:input_pos] == self.input.last_completion + after): @@ -137,38 +142,87 @@ class PrivateTab(OneToOneTab): and not self.input.get_text().startswith('//')) self.send_composing_chat_state(empty_after) + async def handle_message(self, message: SMessage, display: bool = True): + sent = message['from'].bare == self.core.xmpp.boundjid.bare + jid = message['to'] if sent else message['from'] + with_nick = jid.resource + sender_nick = with_nick + if sent: + sender_nick = (self.own_nick or self.core.own_nick) + room_from = jid.bare + use_xhtml = config.get_by_tabname( + 'enable_xhtml_im', + jid.bare + ) + tmp_dir = get_image_cache() + if not sent: + await self.core.events.trigger_async('private_msg', message, self) + body = xhtml.get_body_from_message_stanza( + message, use_xhtml=use_xhtml, extract_images_to=tmp_dir) + if not body or not self: + return + delayed, date = common.find_delayed_tag(message) + replaced = False + user = self.parent_muc.get_user_by_name(with_nick) + if message.get_plugin('replace', check=True): + replaced_id = message['replace']['id'] + if replaced_id != '' and config.get_by_tabname( + 'group_corrections', room_from): + try: + self.modify_message( + body, + replaced_id, + message['id'], + user=user, + time=date, + jid=message['from'], + nickname=sender_nick) + replaced = True + except CorrectionError: + log.debug('Unable to correct a message', exc_info=True) + if not replaced: + msg = Message( + txt=body, + time=date, + history=delayed, + nickname=sender_nick, + nick_color=get_theme().COLOR_OWN_NICK if sent else None, + user=user, + identifier=message['id'], + jid=message['from'], + ) + if display: + self.add_message(msg) + else: + self.log_message(msg) + if sent: + self.set_last_sent_message(message, correct=replaced) + else: + self.last_remote_message = datetime.now() + + @refresh_wrapper.always @command_args_parser.raw - def command_say(self, line, attention=False, correct=False): + async def command_say(self, line: str, attention: bool = False, correct: bool = False) -> None: if not self.on: return - msg = self.core.xmpp.make_message(self.name) + await self._initial_log.wait() + our_jid = JID(self.jid.bare) + our_jid.resource = self.own_nick + msg: SMessage = self.core.xmpp.make_message( + mto=self.jid.full, + mfrom=our_jid, + ) msg['type'] = 'chat' msg['body'] = line + msg.enable('muc') # trigger the event BEFORE looking for colors. # This lets a plugin insert \x19xxx} colors, that will # be converted in xhtml. self.core.events.trigger('private_say', msg, self) if not msg['body']: - self.cancel_paused_delay() - self.text_win.refresh() - self.input.refresh() return - user = self.parent_muc.get_user_by_name(self.own_nick) - replaced = False - if correct or msg['replace']['id']: - msg['replace']['id'] = self.last_sent_message['id'] - if config.get_by_tabname('group_corrections', self.name): - try: - self.modify_message( - msg['body'], - self.last_sent_message['id'], - msg['id'], - user=user, - jid=self.core.xmpp.boundjid, - nickname=self.own_nick) - replaced = True - except: - log.error('Unable to correct a message', exc_info=True) + if correct or msg['replace']['id'] and self.last_sent_message: + msg['replace']['id'] = self.last_sent_message['id'] # type: ignore else: del msg['replace'] @@ -177,43 +231,32 @@ class PrivateTab(OneToOneTab): msg['html']['body'] = xhtml.poezio_colors_to_html(msg['body']) msg['body'] = xhtml.clean_text(msg['body']) if config.get_by_tabname('send_chat_states', self.general_jid): - needed = 'inactive' if self.inactive else 'active' - msg['chat_state'] = needed + if self.inactive: + self.send_chat_state('inactive', always_send=True) + else: + msg['chat_state'] = 'active' if attention: msg['attention'] = True self.core.events.trigger('private_say_after', msg, self) if not msg['body']: - self.cancel_paused_delay() - self.text_win.refresh() - self.input.refresh() return - if not replaced: - self.add_message( - msg['body'], - nickname=self.own_nick or self.core.own_nick, - forced_user=user, - nick_color=get_theme().COLOR_OWN_NICK, - identifier=msg['id'], - jid=self.core.xmpp.boundjid, - typ=1) - - self.last_sent_message = msg - msg._add_receipt = True + self.set_last_sent_message(msg, correct=correct) + await self.core.handler.on_groupchat_private_message(msg, sent=True) + # Our receipts slixmpp hack + msg._add_receipt = True # type: ignore msg.send() self.cancel_paused_delay() - self.text_win.refresh() - self.input.refresh() @command_args_parser.quoted(0, 1) - def command_version(self, args): + async def command_version(self, args): """ /version """ if args: - return self.core.command.version(args[0]) - jid = safeJID(self.name) - self.core.xmpp.plugin['xep_0092'].get_version( - jid, callback=self.core.handler.on_version_result) + return await self.core.command.version(args[0]) + jid = self.jid.full + iq = await self.core.xmpp.plugin['xep_0092'].get_version(jid) + self.core.handler.on_version_result(iq) @command_args_parser.quoted(0, 1) def command_info(self, arg): @@ -223,7 +266,7 @@ class PrivateTab(OneToOneTab): if arg and arg[0]: self.parent_muc.command_info(arg[0]) else: - user = safeJID(self.name).resource + user = self.jid.resource self.parent_muc.command_info(user) def resize(self): @@ -238,8 +281,8 @@ class PrivateTab(OneToOneTab): self.text_win.resize( self.height - 2 - info_win_height - tab_win_height, self.width, 0, - 0) - self.text_win.rebuild_everything(self._text_buffer) + 0, self._text_buffer, force=self.ui_config_changed) + self.ui_config_changed = False self.info_header.resize( 1, self.width, self.height - 2 - info_win_height - tab_win_height, 0) @@ -252,7 +295,7 @@ class PrivateTab(OneToOneTab): display_info_win = not self.size.tab_degrade_y self.text_win.refresh() - self.info_header.refresh(self.name, self.text_win, self.chatstate, + self.info_header.refresh(self.jid.full, self.text_win, self.chatstate, PrivateTab.additional_information) if display_info_win: self.info_win.refresh() @@ -261,12 +304,12 @@ class PrivateTab(OneToOneTab): self.input.refresh() def refresh_info_header(self): - self.info_header.refresh(self.name, self.text_win, self.chatstate, + self.info_header.refresh(self.jid.full, self.text_win, self.chatstate, PrivateTab.additional_information) self.input.refresh() def get_nick(self): - return safeJID(self.name).resource + return self.jid.resource def on_input(self, key, raw): if not raw and key in self.key_func: @@ -278,7 +321,7 @@ class PrivateTab(OneToOneTab): empty_after = self.input.get_text() == '' or ( self.input.get_text().startswith('/') and not self.input.get_text().startswith('//')) - tab = self.core.tabs.by_name_and_class(safeJID(self.name).bare, MucTab) + tab = self.core.tabs.by_name_and_class(self.jid.bare, MucTab) if tab and tab.joined: self.send_composing_chat_state(empty_after) return False @@ -291,7 +334,7 @@ class PrivateTab(OneToOneTab): self.text_win.remove_line_separator() self.text_win.add_line_separator(self._text_buffer) - tab = self.core.tabs.by_name_and_class(safeJID(self.name).bare, MucTab) + tab = self.core.tabs.by_name_and_class(self.jid.bare, MucTab) if tab and tab.joined and config.get_by_tabname( 'send_chat_states', self.general_jid) and self.on: self.send_chat_state('inactive') @@ -300,7 +343,7 @@ class PrivateTab(OneToOneTab): def on_gain_focus(self): self.state = 'current' curses.curs_set(1) - tab = self.core.tabs.by_name_and_class(safeJID(self.name).bare, MucTab) + tab = self.core.tabs.by_name_and_class(self.jid.bare, MucTab) if tab and tab.joined and config.get_by_tabname( 'send_chat_states', self.general_jid, @@ -317,9 +360,6 @@ class PrivateTab(OneToOneTab): 1, self.width, self.height - 2 - self.core.information_win_size - Tab.tab_win_height(), 0) - def get_text_window(self): - return self.text_win - @refresh_wrapper.conditional def rename_user(self, old_nick, user): """ @@ -327,16 +367,18 @@ class PrivateTab(OneToOneTab): display a message. """ self.add_message( - '\x19%(nick_col)s}%(old)s\x19%(info_col)s} is now ' - 'known as \x19%(nick_col)s}%(new)s' % { - 'old': old_nick, - 'new': user.nick, - 'nick_col': dump_tuple(user.color), - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - }, - typ=2) - new_jid = safeJID(self.name).bare + '/' + user.nick - self.name = new_jid + PersistentInfoMessage( + '\x19%(nick_col)s}%(old)s\x19%(info_col)s} is now ' + 'known as \x19%(nick_col)s}%(new)s' % { + 'old': old_nick, + 'new': user.nick, + 'nick_col': dump_tuple(user.color), + 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT) + }, + ), + ) + new_jid = self.jid.bare + '/' + user.nick + self._name = new_jid return self.core.tabs.current_tab is self @refresh_wrapper.conditional @@ -345,36 +387,41 @@ class PrivateTab(OneToOneTab): The user left the associated MUC """ self.deactivate() + theme = get_theme() if config.get_by_tabname('display_user_color_in_join_part', self.general_jid): color = dump_tuple(user.color) else: - color = dump_tuple(get_theme().COLOR_REMOTE_USER) + color = dump_tuple(theme.COLOR_REMOTE_USER) if not status_message: self.add_message( - '\x19%(quit_col)s}%(spec)s \x19%(nick_col)s}' - '%(nick)s\x19%(info_col)s} has left the room' % { - 'nick': user.nick, - 'spec': get_theme().CHAR_QUIT, - 'nick_col': color, - 'quit_col': dump_tuple(get_theme().COLOR_QUIT_CHAR), - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - }, - typ=2) + PersistentInfoMessage( + '\x19%(quit_col)s}%(spec)s \x19%(nick_col)s}' + '%(nick)s\x19%(info_col)s} has left the room' % { + 'nick': user.nick, + 'spec': theme.CHAR_QUIT, + 'nick_col': color, + 'quit_col': dump_tuple(theme.COLOR_QUIT_CHAR), + 'info_col': dump_tuple(theme.COLOR_INFORMATION_TEXT) + }, + ), + ) else: self.add_message( - '\x19%(quit_col)s}%(spec)s \x19%(nick_col)s}' - '%(nick)s\x19%(info_col)s} has left the room' - ' (%(status)s)' % { - 'status': status_message, - 'nick': user.nick, - 'spec': get_theme().CHAR_QUIT, - 'nick_col': color, - 'quit_col': dump_tuple(get_theme().COLOR_QUIT_CHAR), - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - }, - typ=2) + PersistentInfoMessage( + '\x19%(quit_col)s}%(spec)s \x19%(nick_col)s}' + '%(nick)s\x19%(info_col)s} has left the room' + ' (%(status)s)' % { + 'status': status_message, + 'nick': user.nick, + 'spec': theme.CHAR_QUIT, + 'nick_col': color, + 'quit_col': dump_tuple(theme.COLOR_QUIT_CHAR), + 'info_col': dump_tuple(theme.COLOR_INFORMATION_TEXT) + }, + ), + ) return self.core.tabs.current_tab is self @refresh_wrapper.conditional @@ -383,46 +430,51 @@ class PrivateTab(OneToOneTab): The user (or at least someone with the same nick) came back in the MUC """ self.activate() - self.check_features() tab = self.parent_muc - color = dump_tuple(get_theme().COLOR_REMOTE_USER) + theme = get_theme() + color = dump_tuple(theme.COLOR_REMOTE_USER) if tab and config.get_by_tabname('display_user_color_in_join_part', self.general_jid): user = tab.get_user_by_name(nick) if user: color = dump_tuple(user.color) self.add_message( - '\x19%(join_col)s}%(spec)s \x19%(color)s}%(nick)s\x19' - '%(info_col)s} joined the room' % { - 'nick': nick, - 'color': color, - 'spec': get_theme().CHAR_JOIN, - 'join_col': dump_tuple(get_theme().COLOR_JOIN_CHAR), - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - }, - typ=2) + PersistentInfoMessage( + '\x19%(join_col)s}%(spec)s \x19%(color)s}%(nick)s\x19' + '%(info_col)s} joined the room' % { + 'nick': nick, + 'color': color, + 'spec': theme.CHAR_JOIN, + 'join_col': dump_tuple(theme.COLOR_JOIN_CHAR), + 'info_col': dump_tuple(theme.COLOR_INFORMATION_TEXT) + }, + ), + ) return self.core.tabs.current_tab is self def activate(self, reason=None): self.on = True if reason: - self.add_message(txt=reason, typ=2) + self.add_message(PersistentInfoMessage(reason)) def deactivate(self, reason=None): self.on = False if reason: - self.add_message(txt=reason, typ=2) + self.add_message(PersistentInfoMessage(reason)) def matching_names(self): - return [(3, safeJID(self.name).resource), (4, self.name)] + return [(3, self.jid.resource), (4, self.name)] def add_error(self, error_message): - error = '\x19%s}%s\x19o' % (dump_tuple(get_theme().COLOR_CHAR_NACK), + theme = get_theme() + error = '\x19%s}%s\x19o' % (dump_tuple(theme.COLOR_CHAR_NACK), error_message) self.add_message( - error, - highlight=True, - nickname='Error', - nick_color=get_theme().COLOR_ERROR_MSG, - typ=2) + Message( + error, + highlight=True, + nickname='Error', + nick_color=theme.COLOR_ERROR_MSG, + ), + ) self.core.refresh_window() |