Compare commits

...

12 commits

Author SHA1 Message Date
b505c371b0
plugin_e2ee: encrypt: also search tabs for barejids if not found
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-02-28 21:55:42 +01:00
3b81e36242 plugin_e2ee: don't fail on tab.jid if tab is None
I doubt this is the proper fix, there are still paths that use tab below
that.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-02-28 18:08:01 +01:00
8dc4eabdad plugin_e2ee: allow decrypt call to be async
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-02-28 18:08:01 +01:00
mathieui
a87366aa9d fix: do not traceback on encrypted payloads without a tab open 2022-02-28 18:08:01 +01:00
8d178d9af8 bookmark: Add logging on InvalidJid
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-02-28 18:08:01 +01:00
f4185b7485 bookmark nick: Treat empty string as no nick
And prevent the JID() call from failing with InvalidJid because of the
empty resource.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-02-28 18:08:01 +01:00
2efdbf0490 Ensure /bookmark{,_local} and /join use the proper tab object
Now that _add_bookmark is async.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-02-28 18:08:01 +01:00
mathieui
c4112346d4 fix #3553: muclisttab not joining 2022-02-28 18:08:01 +01:00
mathieui
b7b221c988 clean: remove unused imports 2022-02-14 23:32:31 +01:00
mathieui
e4b21822d3 internal: remove unused get_text_window 2022-02-14 23:32:31 +01:00
mathieui
b61db7d0bb internal: make the other message handlers async 2022-02-14 23:32:31 +01:00
mathieui
cf3d06fb0a internal: move and split muc message handling 2022-02-14 23:32:31 +01:00
19 changed files with 226 additions and 179 deletions

View file

@ -1,7 +1,6 @@
from typing import Tuple, Dict, List, Union
import curses
import hashlib
import math
from . import hsluv

View file

@ -14,7 +14,7 @@ from datetime import (
timezone,
)
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union, Any
from typing import Dict, List, Optional, Tuple, Union
import os
import subprocess
@ -23,7 +23,7 @@ import string
import logging
import itertools
from slixmpp import JID, InvalidJID, Message
from slixmpp import Message
from poezio.poezio_shlex import shlex
log = logging.getLogger(__name__)

View file

@ -8,7 +8,7 @@ from xml.etree import ElementTree as ET
from typing import List, Optional, Tuple
import logging
from slixmpp import Iq, JID, InvalidJID
from slixmpp import JID, InvalidJID
from slixmpp.exceptions import XMPPError, IqError, IqTimeout
from slixmpp.xmlstream.xmlstream import NotConnectedError
from slixmpp.xmlstream.stanzabase import StanzaBase
@ -446,13 +446,18 @@ class CommandCore:
"""
/bookmark_local [room][/nick] [password]
"""
if not args and not isinstance(self.core.tabs.current_tab,
tabs.MucTab):
tab = self.core.tabs.current_tab
if not args and not isinstance(tab, tabs.MucTab):
return
room, nick = self._parse_join_jid(args[0] if args else '')
password = args[1] if len(args) > 1 else None
if not room:
room = tab.jid.bare
if password is None and tab.password is not None:
password = tab.password
asyncio.create_task(
self._add_bookmark(
room=room,
@ -468,8 +473,8 @@ class CommandCore:
"""
/bookmark [room][/nick] [autojoin] [password]
"""
if not args and not isinstance(self.core.tabs.current_tab,
tabs.MucTab):
tab = self.core.tabs.current_tab
if not args and not isinstance(tab, tabs.MucTab):
return
room, nick = self._parse_join_jid(args[0] if args else '')
password = args[2] if len(args) > 2 else None
@ -478,13 +483,18 @@ class CommandCore:
autojoin = (method == 'local' or
(len(args) > 1 and args[1].lower() == 'true'))
if not room:
room = tab.jid.bare
if password is None and tab.password is not None:
password = tab.password
asyncio.create_task(
self._add_bookmark(room, nick, autojoin, password, method)
)
async def _add_bookmark(
self,
room: Optional[str],
room: str,
nick: Optional[str],
autojoin: bool,
password: str,
@ -503,16 +513,8 @@ class CommandCore:
method: 'local' or 'remote'.
'''
# No room Jid was specified. A nick may have been specified. Set the
# room Jid to be bookmarked to the current tab bare jid.
if not room:
tab = self.core.tabs.current_tab
if not isinstance(tab, tabs.MucTab):
return
room = tab.jid.bare
if password is None and tab.password is not None:
password = tab.password
elif room == '*':
if room == '*':
return await self._add_wildcard_bookmarks(method)
# Once we found which room to bookmark, find corresponding tab if it
@ -524,13 +526,14 @@ class CommandCore:
# Validate / Normalize
try:
if nick is None:
if not nick:
jid = JID(room)
else:
jid = JID('{}/{}'.format(room, nick))
room = jid.bare
nick = jid.resource or None
except InvalidJID:
self.core.information(f'Invalid address for bookmark: {room}/{nick}', 'Error')
return
bookmark = self.core.bookmarks[room]

View file

@ -15,7 +15,6 @@ import pipes
import sys
import shutil
import time
import uuid
from collections import defaultdict
from typing import (
Any,

View file

@ -4,19 +4,16 @@ XMPP-related handlers for the Core class
import logging
from typing import Optional, Union
from typing import Optional
import asyncio
import curses
import functools
import select
import signal
import ssl
import sys
import time
from datetime import datetime
from hashlib import sha1, sha256, sha512
from os import path
import pyasn1.codec.der.decoder
import pyasn1.codec.der.encoder
@ -25,8 +22,6 @@ from slixmpp import InvalidJID, JID, Message, Iq, Presence
from slixmpp.xmlstream.stanzabase import StanzaBase, ElementBase
from xml.etree import ElementTree as ET
from poezio import common
from poezio import fixes
from poezio import tabs
from poezio import xhtml
from poezio import multiuserchat as muc
@ -36,12 +31,10 @@ from poezio.core.structs import Status
from poezio.contact import Resource
from poezio.logger import logger
from poezio.roster import roster
from poezio.text_buffer import CorrectionError, AckError
from poezio.text_buffer import AckError
from poezio.theming import dump_tuple, get_theme
from poezio.ui.types import (
XMLLog,
Message as PMessage,
BaseMessage,
InfoMessage,
PersistentInfoMessage,
)
@ -363,7 +356,7 @@ class HandlerCore:
)
self.core.tabs.append(conversation)
else:
conversation.handle_message(message)
await conversation.handle_message(message)
if not own and 'private' in config.getstr('beep_on').split():
if not config.get_by_tabname('disable_beep', conv_jid.bare):
@ -478,7 +471,7 @@ class HandlerCore:
else:
contact.name = ''
async def on_groupchat_message(self, message: Message):
async def on_groupchat_message(self, message: Message) -> None:
"""
Triggered whenever a message is received from a multi-user chat room.
"""
@ -495,99 +488,8 @@ class HandlerCore:
muc.leave_groupchat(
self.core.xmpp, room_from, self.core.own_nick, msg='')
return
nick_from = message['mucnick']
user = tab.get_user_by_name(nick_from)
if user and user in tab.ignores:
return
await self.core.events.trigger_async('muc_msg', message, tab)
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
old_state = tab.state
delayed, date = common.find_delayed_tag(message)
is_history = not tab.joined and delayed
replaced = False
if message.xml.find('{urn:xmpp:message-correct:0}replace') is not None:
replaced_id = message['replace']['id']
if replaced_id != '' and config.get_by_tabname(
'group_corrections', message['from'].bare):
try:
delayed_date = date or datetime.now()
if tab.modify_message(
body,
replaced_id,
message['id'],
time=delayed_date,
delayed=delayed,
nickname=nick_from,
user=user):
await self.core.events.trigger_async('highlight', message, tab)
replaced = True
except CorrectionError:
log.debug('Unable to correct a message', exc_info=True)
if not replaced:
ui_msg: Union[InfoMessage, PMessage]
# Messages coming from MUC barejid (Server maintenance, IRC mode
# changes from biboumi, etc.) are displayed as info messages.
highlight = False
if message['from'].resource:
highlight = tab.message_is_highlight(body, nick_from, is_history)
ui_msg = PMessage(
txt=body,
time=date,
nickname=nick_from,
history=is_history,
delayed=delayed,
identifier=message['id'],
jid=message['from'],
user=user,
highlight=highlight,
)
typ = 1
else:
ui_msg = InfoMessage(
txt=body,
time=date,
identifier=message['id'],
)
typ = 2
tab.add_message(ui_msg)
if highlight:
await self.core.events.trigger_async('highlight', message, tab)
if message['from'].resource == tab.own_nick:
tab.set_last_sent_message(message, correct=replaced)
if tab is self.core.tabs.current_tab:
tab.text_win.refresh()
tab.info_header.refresh(tab, tab.text_win, user=tab.own_user)
tab.input.refresh()
self.core.doupdate()
elif tab.state != old_state:
self.core.refresh_tab_win()
current = self.core.tabs.current_tab
if hasattr(current, 'input') and current.input:
current.input.refresh()
self.core.doupdate()
if 'message' in config.getstr('beep_on').split():
valid_message = await tab.handle_message(message)
if valid_message and 'message' in config.getstr('beep_on').split():
if (not config.get_by_tabname('disable_beep', room_from)
and self.core.own_nick != message['from'].resource):
curses.beep()
@ -638,7 +540,7 @@ class HandlerCore:
self.core.tabs.append(tab)
tab.parent_muc.privates.append(tab)
else:
tab.handle_message(message)
await tab.handle_message(message)
if not sent and 'private' in config.getstr('beep_on').split():
if not config.get_by_tabname('disable_beep', jid.full):

View file

@ -5,17 +5,14 @@ upstream.
TODO: Check that they are fixed and remove those hacks
"""
import asyncio
from typing import Callable, Any
from slixmpp import Message, Iq, ClientXMPP
from slixmpp.xmlstream import ET
from slixmpp import ClientXMPP, Message
import logging
log = logging.getLogger(__name__)
def _filter_add_receipt_request(self, stanza):
def _filter_add_receipt_request(self: ClientXMPP, stanza):
"""
Auto add receipt requests to outgoing messages, if:

View file

@ -5,7 +5,6 @@
from __future__ import annotations
import asyncio
import logging
from datetime import datetime, timedelta, timezone
from hashlib import md5

View file

@ -148,9 +148,9 @@ class E2EEPlugin(BasePlugin):
# Ensure decryption is done before everything, so that other handlers
# don't have to know about the encryption mechanism.
self.api.add_event_handler('muc_msg', self._decrypt, priority=0)
self.api.add_event_handler('conversation_msg', self._decrypt, priority=0)
self.api.add_event_handler('private_msg', self._decrypt, priority=0)
self.api.add_event_handler('muc_msg', self._decrypt_wrapper, priority=0)
self.api.add_event_handler('conversation_msg', self._decrypt_wrapper, priority=0)
self.api.add_event_handler('private_msg', self._decrypt_wrapper, priority=0)
# Ensure encryption is done after everything, so that whatever can be
# encrypted is encrypted, and no plain element slips in.
@ -344,6 +344,28 @@ class E2EEPlugin(BasePlugin):
result = await self._encrypt(stanza, passthrough=True)
except NothingToEncrypt:
return stanza
except Exception as exc:
jid = stanza['from']
tab = self.core.tabs.by_name_and_class(jid, ChatTab)
msg = ' \n\x19%s}Could not decrypt message: %s' % (
dump_tuple(get_theme().COLOR_CHAR_NACK),
exc,
)
# XXX: check before commit. Do we not nack in MUCs?
if tab and not isinstance(tab, MucTab):
tab.nack_message(msg, stanza['id'], stanza['to'])
# TODO: display exceptions to the user properly
log.error('Exception in encrypt:', exc_info=True)
return None
return result
async def _decrypt_wrapper(self, stanza: StanzaBase, tab: ChatTabs) -> Optional[StanzaBase]:
"""
Wrapper around _decrypt() to handle errors and display the message after encryption.
"""
try:
# pylint: disable=unexpected-keyword-arg
result = await self._decrypt(stanza, tab, passthrough=True)
except Exception as exc:
jid = stanza['to']
tab = self.core.tabs.by_name_and_class(jid, ChatTab)
@ -352,14 +374,15 @@ class E2EEPlugin(BasePlugin):
exc,
)
# XXX: check before commit. Do we not nack in MUCs?
if not isinstance(tab, MucTab):
if tab and not isinstance(tab, MucTab):
tab.nack_message(msg, stanza['id'], stanza['from'])
# TODO: display exceptions to the user properly
log.error('Exception in encrypt:', exc_info=True)
log.error('Exception in decrypt:', exc_info=True)
return None
return result
def _decrypt(self, message: Message, tab: ChatTabs) -> None:
async def _decrypt(self, message: Message, tab: ChatTabs, passthrough: bool = True) -> None:
has_eme = False
if message.xml.find('{%s}%s' % (EME_NS, EME_TAG)) is not None and \
@ -397,7 +420,14 @@ class E2EEPlugin(BasePlugin):
if user is not None:
jid = user.jid or None
self.decrypt(message, jid, tab)
# Call the enabled encrypt method
func = self.decrypt
if iscoroutinefunction(func):
# pylint: disable=unexpected-keyword-arg
await func(message, jid, tab, passthrough=True)
else:
# pylint: disable=unexpected-keyword-arg
func(message, jid, tab)
log.debug('Decrypted %s message: %r', self.encryption_name, message['body'])
return None
@ -412,10 +442,15 @@ class E2EEPlugin(BasePlugin):
# MUCs). Let the plugin decide what to do with this information.
jids: Optional[List[JID]] = [message['to']]
tab = self.core.tabs.by_jid(message['to'])
if tab is None: # When does that ever happen?
log.debug('Attempting to encrypt a message to \'%s\' '
'that is not attached to a Tab. ?! Aborting '
'encryption.', message['to'])
if tab is None and message['to'].resource:
# Redo the search with the bare JID
tab = self.core.tabs.by_jid(message['to'].bare)
if tab is None: # Possible message sent directly by the e2ee lib?
log.debug(
'A message we do not have a tab for '
'is being sent to \'%s\'. Abort.', message['to'],
)
return None
parent = None
@ -449,7 +484,7 @@ class E2EEPlugin(BasePlugin):
if user.jid.bare:
jids.append(user.jid)
if not self._encryption_enabled(tab.jid):
if tab and not self._encryption_enabled(tab.jid):
raise NothingToEncrypt()
log.debug('Sending %s message', self.encryption_name)

View file

@ -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.
@ -1020,7 +1014,7 @@ class OneToOneTab(ChatTab):
'send a message along with the attention.')
self.init_logs(initial=initial)
def init_logs(self, initial=None) -> None:
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 +1025,19 @@ 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():
async def fallback_no_mam() -> None:
await mam_filler.done.wait()
if mam_filler.result == 0:
self.handle_message(initial)
await self.handle_message(initial)
asyncio.create_task(fallback_no_mam())
elif use_log and initial:
self.handle_message(initial, display=False)
asyncio.create_task(self.handle_message(initial, display=False))
asyncio.create_task(
LogLoader(logger, self, use_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):

View file

@ -22,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
@ -106,7 +105,7 @@ 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).
@ -133,7 +132,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
@ -439,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):

View file

@ -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]))

View file

@ -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.
@ -452,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:
@ -482,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', 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()

View file

@ -142,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
@ -156,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:
@ -361,9 +361,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):
"""

View file

@ -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

View file

@ -73,7 +73,6 @@ except ImportError:
import curses
import functools
import os
from typing import Dict, List, Union, Tuple, Optional, cast
from pathlib import Path
from os import path

View file

@ -1,5 +1,3 @@
from datetime import datetime
FORMAT_CHAR = '\x19'
# These are non-printable chars, so they should never appear in the input,
# I guess. But maybe we can find better chars that are even less risky.

View file

@ -12,7 +12,6 @@ A user is a MUC participant, not a roster contact (see contact.py)
import logging
from datetime import timedelta, datetime
from hashlib import md5
from random import choice
from typing import Optional, Tuple
from poezio import xhtml, colors

View file

@ -5,7 +5,7 @@ Text inputs.
import curses
import logging
import string
from typing import List, Dict, Callable, Optional, ClassVar, Union
from typing import List, Dict, Callable, Optional, ClassVar
from poezio import keyboard
from poezio import common

View file

@ -6,11 +6,10 @@ import logging
log = logging.getLogger(__name__)
from datetime import datetime
from typing import Optional, List, Union, Dict
from typing import Optional, List, Union
from poezio.windows.base_wins import Win
from poezio import common
from poezio.config import config
from poezio.contact import Contact, Resource
from poezio.roster import Roster, RosterGroup