diff options
Diffstat (limited to 'poezio/tabs/conversationtab.py')
-rw-r--r-- | poezio/tabs/conversationtab.py | 288 |
1 files changed, 168 insertions, 120 deletions
diff --git a/poezio/tabs/conversationtab.py b/poezio/tabs/conversationtab.py index 94f1d719..de1f988a 100644 --- a/poezio/tabs/conversationtab.py +++ b/poezio/tabs/conversationtab.py @@ -11,45 +11,46 @@ There are two different instances of a ConversationTab: the time. """ +import asyncio import curses import logging +from datetime import datetime from typing import Dict, Callable +from slixmpp import JID, InvalidJID, Message as SMessage + from poezio.tabs.basetabs import OneToOneTab, 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.roster import roster -from poezio.text_buffer import CorrectionError from poezio.theming import get_theme, dump_tuple from poezio.decorators import command_args_parser +from poezio.ui.types import InfoMessage, Message +from poezio.text_buffer import CorrectionError log = logging.getLogger(__name__) class ConversationTab(OneToOneTab): """ - The tab containg a normal conversation (not from a MUC) + The tab containing a normal conversation (not from a MUC) Must not be instantiated, use Static or Dynamic version only. """ - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] - additional_information = {} # type: Dict[str, Callable[[str], str]] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} + additional_information: Dict[str, Callable[[str], str]] = {} message_type = 'chat' - def __init__(self, core, jid): - OneToOneTab.__init__(self, core, jid) + def __init__(self, core, jid, initial=None): + OneToOneTab.__init__(self, core, jid, initial=initial) self.nick = None self.nick_sent = False self.state = 'normal' - self.name = jid # a conversation tab is linked to one specific full jid OR bare jid - self.text_win = windows.TextWin() - self._text_buffer.add_window(self.text_win) self.upper_bar = windows.ConversationStatusMessageWin() self.input = windows.MessageInput() # keys @@ -73,13 +74,6 @@ class ConversationTab(OneToOneTab): shortdesc='Get the activity.', completion=self.core.completion.last_activity) self.register_command( - 'add', - self.command_add, - desc='Add the current 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( 'invite', self.core.command.impromptu, desc='Invite people into an impromptu room.', @@ -89,13 +83,14 @@ class ConversationTab(OneToOneTab): self.update_keys() @property - def general_jid(self): - return safeJID(self.name).bare + def general_jid(self) -> JID: + return JID(self.jid.bare) def get_info_header(self): raise NotImplementedError @staticmethod + @refresh_wrapper.always def add_information_element(plugin_name, callback): """ Lets a plugin add its own information to the ConversationInfoWin @@ -103,15 +98,95 @@ class ConversationTab(OneToOneTab): ConversationTab.additional_information[plugin_name] = callback @staticmethod + @refresh_wrapper.always def remove_information_element(plugin_name): del ConversationTab.additional_information[plugin_name] def completion(self): self.complete_commands(self.input) + async def handle_message(self, message: SMessage, display: bool = True): + """Handle a received message. + + The message can come from us (carbon copy). + """ + + # Prevent messages coming from our own devices (1:1) to be reflected + if message['to'].bare == self.core.xmpp.boundjid.bare and \ + message['from'].bare == self.core.xmpp.boundjid.bare: + _, index = self._text_buffer._find_message(message['id']) + if index != -1: + return + + use_xhtml = config.get_by_tabname( + 'enable_xhtml_im', + message['from'].bare + ) + tmp_dir = get_image_cache() + + # normal message, we are the recipient + if message['to'].bare == self.core.xmpp.boundjid.bare: + conv_jid = message['from'] + jid = conv_jid + color = get_theme().COLOR_REMOTE_USER + self.last_remote_message = datetime.now() + remote_nick = self.get_nick() + # we wrote the message (happens with carbons) + elif message['from'].bare == self.core.xmpp.boundjid.bare: + conv_jid = message['to'] + jid = self.core.xmpp.boundjid + color = get_theme().COLOR_OWN_NICK + remote_nick = self.core.own_nick + # we are not part of that message, drop it + else: + return + + await self.core.events.trigger_async('conversation_msg', message, self) + + if not message['body']: + return + body = xhtml.get_body_from_message_stanza( + message, use_xhtml=use_xhtml, extract_images_to=tmp_dir) + delayed, date = common.find_delayed_tag(message) + + replaced = False + if message.get_plugin('replace', check=True): + replaced_id = message['replace']['id'] + if replaced_id and config.get_by_tabname('group_corrections', + conv_jid.bare): + try: + replaced = self.modify_message( + body, + replaced_id, + message['id'], + time=date, + jid=jid, + nickname=remote_nick) + except CorrectionError: + log.debug('Unable to correct the message: %s', message) + if not replaced: + msg = Message( + txt=body, + time=date, + nickname=remote_nick, + nick_color=color, + history=delayed, + identifier=message['id'], + jid=jid, + ) + if display: + self.add_message(msg) + else: + self.log_message(msg) + + @refresh_wrapper.always @command_args_parser.raw - def command_say(self, line, attention=False, correct=False): - msg = self.core.xmpp.make_message(self.get_dest_jid()) + async def command_say(self, line: str, attention: bool = False, correct: bool = False): + await self._initial_log.wait() + msg: SMessage = self.core.xmpp.make_message( + mto=self.get_dest_jid(), + mfrom=self.core.xmpp.boundjid + ) msg['type'] = 'chat' msg['body'] = line if not self.nick_sent: @@ -123,24 +198,9 @@ class ConversationTab(OneToOneTab): # be converted in xhtml. self.core.events.trigger('conversation_say', msg, self) if not msg['body']: - self.cancel_paused_delay() - self.text_win.refresh() - self.input.refresh() return - 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'], - jid=self.core.xmpp.boundjid, - nickname=self.core.own_nick) - replaced = True - except CorrectionError: - log.error('Unable to correct a message', exc_info=True) + msg['replace']['id'] = self.last_sent_message['id'] # type: ignore else: del msg['replace'] if msg['body'].find('\x19') != -1: @@ -148,31 +208,21 @@ class ConversationTab(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('conversation_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.core.own_nick, - 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) + msg._add_receipt = True # type: ignore msg.send() + await self.core.handler.on_normal_message(msg) + # Our receipts slixmpp hack self.cancel_paused_delay() - self.text_win.refresh() - self.input.refresh() @command_args_parser.quoted(0, 1) def command_last_activity(self, args): @@ -196,7 +246,13 @@ class ConversationTab(OneToOneTab): status = iq['last_activity']['status'] from_ = iq['from'] msg = '\x19%s}The last activity of %s was %s ago%s' - if not safeJID(from_).user: + user = '' + try: + user = JID(from_).user + except InvalidJID: + pass + + if not user: msg = '\x19%s}The uptime of %s is %s.' % ( dump_tuple(get_theme().COLOR_INFORMATION_TEXT), from_, common.parse_secs_to_str(seconds)) @@ -205,10 +261,10 @@ class ConversationTab(OneToOneTab): dump_tuple(get_theme().COLOR_INFORMATION_TEXT), from_, common.parse_secs_to_str(seconds), - (' and his/her last status was %s' % status) + (' and their last status was %s' % status) if status else '', ) - self.add_message(msg) + self.add_message(InfoMessage(msg)) self.core.refresh_window() self.core.xmpp.plugin['xep_0012'].get_last_activity( @@ -218,7 +274,10 @@ class ConversationTab(OneToOneTab): @command_args_parser.ignored def command_info(self): contact = roster[self.get_dest_jid()] - jid = safeJID(self.get_dest_jid()) + try: + jid = JID(self.get_dest_jid()) + except InvalidJID: + jid = JID('') if contact: if jid.resource: resource = contact[jid.full] @@ -227,48 +286,29 @@ class ConversationTab(OneToOneTab): else: resource = None if resource: - status = ( - 'Status: %s' % resource.status) if resource.status else '' - self._text_buffer.add_message( - "\x19%(info_col)s}Show: %(show)s, %(status)s\x19o" % { - 'show': resource.presence or 'available', - 'status': status, - 'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT) - }) - return True - else: - self._text_buffer.add_message( - "\x19%(info_col)s}No information available\x19o" % - {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)}) + status = (f', Status: {resource.status}') if resource.status else '' + show = f"Show: {resource.presence or 'available'}" + self.add_message(InfoMessage(f'{show}{status}')) return True + self.add_message( + InfoMessage("No information available"), + ) + return True @command_args_parser.quoted(0, 1) - def command_version(self, args): + async def command_version(self, args): """ /version [jid] """ if args: - return self.core.command.version(args[0]) - jid = safeJID(self.name) + return await self.core.command.version(args[0]) + jid = self.jid if not jid.resource: if jid in roster: resource = roster[jid].get_highest_priority_resource() jid = resource.jid if resource else jid - self.core.xmpp.plugin['xep_0092'].get_version( - jid, callback=self.core.handler.on_version_result) - - @command_args_parser.ignored - def command_add(self): - """ - Add the current JID to the roster, and automatically - accept the reverse subscription - """ - jid = self.general_jid - if jid in roster and roster[jid].subscription in ('to', 'both'): - return self.core.information('Already subscribed.', 'Roster') - roster.add(jid) - roster.modified() - self.core.information('%s was added to the roster' % jid, 'Roster') + iq = await self.core.xmpp.plugin['xep_0092'].get_version(jid) + self.core.handler.on_version_result(iq) def resize(self): self.need_resize = False @@ -285,8 +325,10 @@ class ConversationTab(OneToOneTab): self.text_win.resize( self.height - 2 - bar_height - info_win_height - tab_win_height, - self.width, bar_height, 0) - self.text_win.rebuild_everything(self._text_buffer) + self.width, bar_height, 0, self._text_buffer, + force=self.ui_config_changed + ) + self.ui_config_changed = False if display_bar: self.upper_bar.resize(1, self.width, 0, 0) self.get_info_header().resize( @@ -321,14 +363,13 @@ class ConversationTab(OneToOneTab): self.input.refresh() def get_nick(self): - jid = safeJID(self.name) - contact = roster[jid.bare] + contact = roster[self.jid.bare] if contact: - return contact.name or jid.user + return contact.name or self.jid.user else: if self.nick: return self.nick - return jid.user + return self.jid.user or self.jid.domain def on_input(self, key, raw): if not raw and key in self.key_func: @@ -343,7 +384,10 @@ class ConversationTab(OneToOneTab): def on_lose_focus(self): contact = roster[self.get_dest_jid()] - jid = safeJID(self.get_dest_jid()) + try: + jid = JID(self.get_dest_jid()) + except InvalidJID: + jid = JID('') if contact: if jid.resource: resource = contact[jid.full] @@ -364,7 +408,10 @@ class ConversationTab(OneToOneTab): def on_gain_focus(self): contact = roster[self.get_dest_jid()] - jid = safeJID(self.get_dest_jid()) + try: + jid = JID(self.get_dest_jid()) + except InvalidJID: + jid = JID('') if contact: if jid.resource: resource = contact[jid.full] @@ -391,9 +438,6 @@ class ConversationTab(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 - def on_close(self): Tab.on_close(self) if config.get_by_tabname('send_chat_states', self.general_jid): @@ -401,7 +445,7 @@ class ConversationTab(OneToOneTab): def matching_names(self): res = [] - jid = safeJID(self.name) + jid = self.jid res.append((2, jid.bare)) res.append((1, jid.user)) contact = roster[self.name] @@ -417,13 +461,13 @@ class DynamicConversationTab(ConversationTab): bad idea so it has been removed. Only one DynamicConversationTab can be opened for a given jid. """ - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} - def __init__(self, core, jid, resource=None): + def __init__(self, core, jid, initial=None): self.locked_resource = None - self.name = safeJID(jid).bare - ConversationTab.__init__(self, core, jid) + ConversationTab.__init__(self, core, jid, initial=initial) + self.jid.resource = None self.info_header = windows.DynamicConversationInfoWin() self.register_command( 'unlock', self.unlock_command, shortdesc='Deprecated, do nothing.') @@ -447,7 +491,7 @@ class DynamicConversationTab(ConversationTab): """ Returns the bare jid. """ - return self.name + return self.jid.bare def refresh(self): """ @@ -460,9 +504,9 @@ class DynamicConversationTab(ConversationTab): self.text_win.refresh() if display_bar: - self.upper_bar.refresh(self.name, roster[self.name]) - displayed_jid = self.name - self.get_info_header().refresh(displayed_jid, roster[self.name], + self.upper_bar.refresh(self.jid.bare, roster[self.jid.bare]) + displayed_jid = self.jid.bare + self.get_info_header().refresh(displayed_jid, roster[self.jid.bare], self.text_win, self.chatstate, ConversationTab.additional_information) if display_info_win: @@ -475,8 +519,8 @@ class DynamicConversationTab(ConversationTab): """ Different from the parent class only for the info_header object. """ - displayed_jid = self.name - self.get_info_header().refresh(displayed_jid, roster[self.name], + displayed_jid = self.jid.bare + self.get_info_header().refresh(displayed_jid, roster[self.jid.bare], self.text_win, self.chatstate, ConversationTab.additional_information) self.input.refresh() @@ -487,16 +531,20 @@ class StaticConversationTab(ConversationTab): A conversation tab associated with one Full JID. It cannot be locked to an different resource or unlocked. """ - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} - def __init__(self, core, jid): - assert (safeJID(jid).resource) - ConversationTab.__init__(self, core, jid) + def __init__(self, core, jid, initial=None): + ConversationTab.__init__(self, core, jid, initial=initial) + assert jid.resource self.info_header = windows.ConversationInfoWin() self.resize() self.update_commands() self.update_keys() + async def init_logs(self, initial=None) -> None: + # Disable local logs becauseā¦ + pass + def get_info_header(self): return self.info_header |