diff options
Diffstat (limited to 'poezio/tabs')
-rw-r--r-- | poezio/tabs/basetabs.py | 59 | ||||
-rw-r--r-- | poezio/tabs/bookmarkstab.py | 2 | ||||
-rw-r--r-- | poezio/tabs/conversationtab.py | 45 | ||||
-rw-r--r-- | poezio/tabs/muclisttab.py | 3 | ||||
-rw-r--r-- | poezio/tabs/muctab.py | 170 | ||||
-rw-r--r-- | poezio/tabs/privatetab.py | 21 | ||||
-rw-r--r-- | poezio/tabs/rostertab.py | 4 | ||||
-rw-r--r-- | poezio/tabs/xmltab.py | 1 |
8 files changed, 222 insertions, 83 deletions
diff --git a/poezio/tabs/basetabs.py b/poezio/tabs/basetabs.py index 508465e3..793eae62 100644 --- a/poezio/tabs/basetabs.py +++ b/poezio/tabs/basetabs.py @@ -170,15 +170,15 @@ class Tab: 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 @@ -351,7 +351,7 @@ class Tab: if hasattr(self.input, "reset_completion"): self.input.reset_completion() if asyncio.iscoroutinefunction(func): - asyncio.ensure_future(func(arg)) + asyncio.create_task(func(arg)) else: func(arg) return True @@ -390,12 +390,6 @@ 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. @@ -498,7 +492,7 @@ class GapTab(Tab): return 0 @property - def name(self): + def name(self) -> str: return '' def refresh(self): @@ -520,6 +514,7 @@ class ChatTab(Tab): 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: Union[JID, str]): Tab.__init__(self, core) @@ -675,7 +670,9 @@ class ChatTab(Tab): 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 @@ -800,7 +797,7 @@ class ChatTab(Tab): self.last_sent_message = msg @command_args_parser.raw - def command_correct(self, line: str) -> None: + async def command_correct(self, line: str) -> None: """ /correct <fixed message> """ @@ -810,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: @@ -844,7 +841,7 @@ class ChatTab(Tab): self.state = 'scrolled' @command_args_parser.raw - def command_say(self, line: str, attention: bool = False, correct: bool = False): + async def command_say(self, line: str, attention: bool = False, correct: bool = False): pass def goto_build_lines(self, new_date): @@ -979,7 +976,7 @@ class ChatTab(Tab): def on_scroll_up(self): if not self.query_status: from poezio.log_loader import LogLoader - asyncio.ensure_future( + asyncio.create_task( LogLoader(logger, self, config.getbool('use_log')).scroll_requested() ) return self.text_win.scroll_up(self.text_win.height - 1) @@ -1004,6 +1001,7 @@ class OneToOneTab(ChatTab): 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 @@ -1018,9 +1016,9 @@ class OneToOneTab(ChatTab): shortdesc='Request the attention.', desc='Attention: Request the attention of the contact. Can also ' 'send a message along with the attention.') - self.init_logs(initial=initial) + asyncio.create_task(self.init_logs(initial=initial)) - def init_logs(self, initial=None) -> None: + 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: @@ -1031,19 +1029,16 @@ class OneToOneTab(ChatTab): 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 - async def fallback_no_mam(): - await mam_filler.done.wait() - if mam_filler.result == 0: - self.handle_message(initial) - - asyncio.ensure_future(fallback_no_mam()) + await mam_filler.done.wait() + if mam_filler.result == 0: + await self.handle_message(initial) elif use_log and initial: - self.handle_message(initial, display=False) - asyncio.ensure_future( - LogLoader(logger, self, use_log).tab_open() - ) + 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() - def handle_message(self, msg: SMessage, display: bool = True): + async def handle_message(self, msg: SMessage, display: bool = True): pass def remote_user_color(self): @@ -1116,10 +1111,10 @@ class OneToOneTab(ChatTab): self.refresh() @command_args_parser.raw - def command_attention(self, message): + async def command_attention(self, message): """/attention [message]""" if message != '': - self.command_say(message, attention=True) + await self.command_say(message, attention=True) else: msg = self.core.xmpp.make_message(self.get_dest_jid()) msg['type'] = 'chat' @@ -1127,7 +1122,7 @@ class OneToOneTab(ChatTab): msg.send() @command_args_parser.raw - def command_say(self, line: str, attention: bool = False, correct: bool = False): + async def command_say(self, line: str, attention: bool = False, correct: bool = False): pass @command_args_parser.ignored diff --git a/poezio/tabs/bookmarkstab.py b/poezio/tabs/bookmarkstab.py index 10c7c0ce..d21b5630 100644 --- a/poezio/tabs/bookmarkstab.py +++ b/poezio/tabs/bookmarkstab.py @@ -96,7 +96,7 @@ class BookmarksTab(Tab): if bm in self.bookmarks: self.bookmarks.remove(bm) - asyncio.ensure_future( + asyncio.create_task( self.save_routine() ) diff --git a/poezio/tabs/conversationtab.py b/poezio/tabs/conversationtab.py index 9ddb6fc1..de1f988a 100644 --- a/poezio/tabs/conversationtab.py +++ b/poezio/tabs/conversationtab.py @@ -11,6 +11,7 @@ There are two different instances of a ConversationTab: the time. """ +import asyncio import curses import logging from datetime import datetime @@ -21,7 +22,6 @@ from slixmpp import JID, InvalidJID, Message as SMessage from poezio.tabs.basetabs import OneToOneTab, Tab from poezio import common -from poezio import tabs from poezio import windows from poezio import xhtml from poezio.config import config, get_image_cache @@ -83,8 +83,8 @@ class ConversationTab(OneToOneTab): self.update_keys() @property - def general_jid(self): - return self.jid.bare + def general_jid(self) -> JID: + return JID(self.jid.bare) def get_info_header(self): raise NotImplementedError @@ -105,16 +105,25 @@ class ConversationTab(OneToOneTab): def completion(self): self.complete_commands(self.input) - def handle_message(self, message: SMessage, display: bool = True): + 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'] @@ -132,7 +141,7 @@ class ConversationTab(OneToOneTab): else: return - self.core.events.trigger('conversation_msg', message, self) + await self.core.events.trigger_async('conversation_msg', message, self) if not message['body']: return @@ -172,7 +181,8 @@ class ConversationTab(OneToOneTab): @refresh_wrapper.always @command_args_parser.raw - def command_say(self, line: str, attention: bool = False, correct: bool = False): + 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 @@ -189,7 +199,6 @@ class ConversationTab(OneToOneTab): self.core.events.trigger('conversation_say', msg, self) if not msg['body']: return - replaced = False if correct or msg['replace']['id']: msg['replace']['id'] = self.last_sent_message['id'] # type: ignore else: @@ -209,10 +218,10 @@ class ConversationTab(OneToOneTab): if not msg['body']: return self.set_last_sent_message(msg, correct=correct) - self.core.handler.on_normal_message(msg) - # Our receipts slixmpp hack msg._add_receipt = True # type: ignore msg.send() + await self.core.handler.on_normal_message(msg) + # Our receipts slixmpp hack self.cancel_paused_delay() @command_args_parser.quoted(0, 1) @@ -277,16 +286,9 @@ class ConversationTab(OneToOneTab): else: resource = None if resource: - status = ( - 'Status: %s' % resource.status) if resource.status else '' - self.add_message( - InfoMessage( - "Show: %(show)s, %(status)s" % { - 'show': resource.presence or 'available', - 'status': status, - } - ), - ) + 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"), @@ -436,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): @@ -543,7 +542,7 @@ class StaticConversationTab(ConversationTab): self.update_commands() self.update_keys() - def init_logs(self, initial=None) -> None: + async def init_logs(self, initial=None) -> None: # Disable local logs because… pass diff --git a/poezio/tabs/muclisttab.py b/poezio/tabs/muclisttab.py index f6b3fc35..53fce727 100644 --- a/poezio/tabs/muclisttab.py +++ b/poezio/tabs/muclisttab.py @@ -4,6 +4,7 @@ A MucListTab is a tab listing the rooms on a conference server. It has no functionality except scrolling the list, and allowing the user to join the rooms. """ +import asyncio import logging from typing import Dict, Callable @@ -74,4 +75,4 @@ class MucListTab(ListTab): row = self.listview.get_selected_row() if not row: return - self.core.command.join(row[1]) + asyncio.ensure_future(self.core.command.join(row[1])) diff --git a/poezio/tabs/muctab.py b/poezio/tabs/muctab.py index acc145af..e2d546c9 100644 --- a/poezio/tabs/muctab.py +++ b/poezio/tabs/muctab.py @@ -18,6 +18,7 @@ import random import re import functools from copy import copy +from dataclasses import dataclass from datetime import datetime from typing import ( cast, @@ -44,12 +45,13 @@ from poezio import timed_events from poezio import windows from poezio import xhtml from poezio.common import to_utc -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, command_args_parser from poezio.logger import logger from poezio.log_loader import LogLoader, MAMFiller from poezio.roster import roster +from poezio.text_buffer import CorrectionError from poezio.theming import get_theme, dump_tuple from poezio.user import User from poezio.core.structs import Completion, Status @@ -73,6 +75,18 @@ NS_MUC_USER = 'http://jabber.org/protocol/muc#user' COMPARE_USERS_LAST_TALKED = lambda x: x.last_talked +@dataclass +class MessageData: + message: SMessage + delayed: bool + date: Optional[datetime] + nick: str + user: Optional[User] + room_from: str + body: str + is_history: bool + + class MucTab(ChatTab): """ The tab containing a multi-user-chat room. @@ -154,14 +168,14 @@ class MucTab(ChatTab): """ The user do not want to send their config, send an iq cancel """ - asyncio.ensure_future(self.core.xmpp['xep_0045'].cancel_config(self.jid)) + asyncio.create_task(self.core.xmpp['xep_0045'].cancel_config(self.jid)) self.core.close_tab() def send_config(self, form: Form) -> None: """ The user sends their config to the server """ - asyncio.ensure_future(self.core.xmpp['xep_0045'].set_room_config(self.jid, form)) + asyncio.create_task(self.core.xmpp['xep_0045'].set_room_config(self.jid, form)) self.core.close_tab() def join(self) -> None: @@ -233,6 +247,8 @@ class MucTab(ChatTab): message) self.core.disable_private_tabs(self.jid.bare, reason=msg) else: + self.presence_buffer = [] + self.users = [] muc.leave_groupchat(self.core.xmpp, self.jid, self.own_nick, message) @@ -450,9 +466,6 @@ class MucTab(ChatTab): # TODO: send the disco#info identity name here, if it exists. return self.jid.node - def get_text_window(self) -> windows.TextWin: - return self.text_win - def on_lose_focus(self) -> None: if self.joined: if self.input.text: @@ -480,6 +493,126 @@ class MucTab(ChatTab): self.general_jid) and not self.input.get_text(): self.send_chat_state('active') + async def handle_message(self, message: SMessage) -> bool: + """Parse an incoming message + + Returns False if the message was dropped silently. + """ + room_from = message['from'].bare + nick_from = message['mucnick'] + user = self.get_user_by_name(nick_from) + if user and user in self.ignores: + return False + + await self.core.events.trigger_async('muc_msg', message, self) + use_xhtml = config.get_by_tabname('enable_xhtml_im', room_from) + tmp_dir = get_image_cache() + body = xhtml.get_body_from_message_stanza( + message, use_xhtml=use_xhtml, extract_images_to=tmp_dir) + + # TODO: #3314. Is this a MUC reflection? + # Is this an encrypted message? Is so ignore. + # It is not possible in the OMEMO case to decrypt these messages + # since we don't encrypt for our own device (something something + # forward secrecy), but even for non-FS encryption schemes anyway + # messages shouldn't have changed after a round-trip to the room. + # Otherwire replace the matching message we sent. + if not body: + return False + + old_state = self.state + delayed, date = common.find_delayed_tag(message) + is_history = not self.joined and delayed + + mdata = MessageData( + message, delayed, date, nick_from, user, room_from, body, + is_history + ) + + replaced = False + if message.xml.find('{urn:xmpp:message-correct:0}replace') is not None: + replaced = await self._handle_correction_message(mdata) + if not replaced: + await self._handle_normal_message(mdata) + if mdata.nick == self.own_nick: + self.set_last_sent_message(message, correct=replaced) + self._refresh_after_message(old_state) + return True + + def _refresh_after_message(self, old_state: str) -> None: + """Refresh the appropriate UI after a message is received""" + if self is self.core.tabs.current_tab: + self.refresh() + elif self.state != old_state: + self.core.refresh_tab_win() + current = self.core.tabs.current_tab + current.refresh_input() + self.core.doupdate() + + async def _handle_correction_message(self, message: MessageData) -> bool: + """Process a correction message. + + Returns true if a message was actually corrected. + """ + replaced_id = message.message['replace']['id'] + if replaced_id != '' and config.get_by_tabname( + 'group_corrections', JID(message.room_from)): + try: + delayed_date = message.date or datetime.now() + modify_hl = self.modify_message( + message.body, + replaced_id, + message.message['id'], + time=delayed_date, + delayed=message.delayed, + nickname=message.nick, + user=message.user + ) + if modify_hl: + await self.core.events.trigger_async( + 'highlight', + message.message, + self + ) + return True + except CorrectionError: + log.debug('Unable to correct a message', exc_info=True) + return False + + async def _handle_normal_message(self, message: MessageData) -> None: + """ + Process the non-correction groupchat message. + """ + ui_msg: Union[InfoMessage, Message] + # Messages coming from MUC barejid (Server maintenance, IRC mode + # changes from biboumi, etc.) have no nick/resource and are displayed + # as info messages. + highlight = False + if message.nick: + highlight = self.message_is_highlight( + message.body, message.nick, message.is_history + ) + ui_msg = Message( + txt=message.body, + time=message.date, + nickname=message.nick, + history=message.is_history, + delayed=message.delayed, + identifier=message.message['id'], + jid=message.message['from'], + user=message.user, + highlight=highlight, + ) + else: + ui_msg = InfoMessage( + txt=message.body, + time=message.date, + identifier=message.message['id'], + ) + self.add_message(ui_msg) + if highlight: + await self.core.events.trigger_async('highlight', message, self) + def handle_presence(self, presence: Presence) -> None: """Handle MUC presence""" self.reset_lag() @@ -610,7 +743,7 @@ class MucTab(ChatTab): }, ), ) - asyncio.ensure_future(LogLoader( + asyncio.create_task(LogLoader( logger, self, config.get_by_tabname('use_log', self.general_jid) ).tab_open()) @@ -662,6 +795,17 @@ class MucTab(ChatTab): elif typ == 'unavailable': self.on_user_leave_groupchat(user, jid, status, from_nick, JID(from_room), server_initiated) + ns = 'http://jabber.org/protocol/muc#user' + if presence.xml.find(f'{{{ns}}}x/{{{ns}}}destroy') is not None: + info = f'Room {self.jid} was destroyed.' + if presence['muc']['destroy']: + reason = presence['muc']['destroy']['reason'] + altroom = presence['muc']['destroy']['jid'] + if reason: + info += f' “{reason}”.' + if altroom: + info += f' The new address now is {altroom}.' + self.core.information(info, 'Info') # status change else: self.on_user_change_status(user, from_nick, from_room, affiliation, @@ -1513,7 +1657,7 @@ class MucTab(ChatTab): bookmark = self.core.bookmarks[self.jid] if bookmark: bookmark.autojoin = False - asyncio.ensure_future( + asyncio.create_task( self.core.bookmarks.save(self.core.xmpp) ) self.core.close_tab(self) @@ -1538,8 +1682,10 @@ class MucTab(ChatTab): r = self.core.open_private_window(self.jid.bare, user.nick) if r and len(args) == 2: msg = args[1] - r.command_say( - xhtml.convert_simple_to_full_colors(msg) + asyncio.ensure_future( + r.command_say( + xhtml.convert_simple_to_full_colors(msg) + ) ) if not r: self.core.information("Cannot find user: %s" % nick, 'Error') @@ -1712,7 +1858,7 @@ class MucTab(ChatTab): return None @command_args_parser.raw - def command_say(self, line: str, attention: bool = False, correct: bool = False): + async def command_say(self, line: str, attention: bool = False, correct: bool = False): """ /say <message> Or normal input + enter @@ -2198,7 +2344,7 @@ class MucTab(ChatTab): 'shortdesc': 'Fix a color for a nick.', 'completion': - self.completion_recolor + self.completion_color }, { 'name': 'cycle', diff --git a/poezio/tabs/privatetab.py b/poezio/tabs/privatetab.py index fb89d8e6..1909e3c1 100644 --- a/poezio/tabs/privatetab.py +++ b/poezio/tabs/privatetab.py @@ -10,6 +10,7 @@ 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 @@ -47,7 +48,7 @@ class PrivateTab(OneToOneTab): additional_information: Dict[str, Callable[[str], str]] = {} def __init__(self, core, jid, nick, initial=None): - OneToOneTab.__init__(self, core, jid) + OneToOneTab.__init__(self, core, jid, initial) self.own_nick = nick self.info_header = windows.PrivateInfoWin() self.input = windows.MessageInput() @@ -84,14 +85,14 @@ class PrivateTab(OneToOneTab): return super().remote_user_color() @property - def general_jid(self): + def general_jid(self) -> JID: return self.jid - def get_dest_jid(self): + 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): @@ -141,7 +142,7 @@ class PrivateTab(OneToOneTab): and not self.input.get_text().startswith('//')) self.send_composing_chat_state(empty_after) - def handle_message(self, message: SMessage, display: bool = True): + 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 @@ -155,7 +156,7 @@ class PrivateTab(OneToOneTab): ) tmp_dir = get_image_cache() if not sent: - self.core.events.trigger('private_msg', message, self) + 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: @@ -201,9 +202,10 @@ class PrivateTab(OneToOneTab): @refresh_wrapper.always @command_args_parser.raw - def command_say(self, line: str, attention: bool = False, correct: bool = False) -> None: + async def command_say(self, line: str, attention: bool = False, correct: bool = False) -> None: if not self.on: return + await self._initial_log.wait() our_jid = JID(self.jid.bare) our_jid.resource = self.own_nick msg: SMessage = self.core.xmpp.make_message( @@ -239,7 +241,7 @@ class PrivateTab(OneToOneTab): if not msg['body']: return self.set_last_sent_message(msg, correct=correct) - self.core.handler.on_groupchat_private_message(msg, sent=True) + await self.core.handler.on_groupchat_private_message(msg, sent=True) # Our receipts slixmpp hack msg._add_receipt = True # type: ignore msg.send() @@ -358,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): """ diff --git a/poezio/tabs/rostertab.py b/poezio/tabs/rostertab.py index 66aff2b1..18334c20 100644 --- a/poezio/tabs/rostertab.py +++ b/poezio/tabs/rostertab.py @@ -14,7 +14,7 @@ import ssl from functools import partial from os import getenv, path from pathlib import Path -from typing import Dict, Callable +from typing import Dict, Callable, Union from slixmpp import JID, InvalidJID from slixmpp.exceptions import IqError, IqTimeout @@ -199,7 +199,7 @@ class RosterInfoTab(Tab): completion=self.completion_cert_fetch) @property - def selected_row(self): + def selected_row(self) -> Union[Contact, Resource]: return self.roster_win.get_selected_row() @command_args_parser.ignored diff --git a/poezio/tabs/xmltab.py b/poezio/tabs/xmltab.py index 9501c6d3..939af67d 100644 --- a/poezio/tabs/xmltab.py +++ b/poezio/tabs/xmltab.py @@ -10,7 +10,6 @@ log = logging.getLogger(__name__) import curses import os -from typing import Union, Optional from slixmpp import JID, InvalidJID from slixmpp.xmlstream import matcher, StanzaBase from slixmpp.xmlstream.tostring import tostring |