diff options
Diffstat (limited to 'poezio/core/handlers.py')
-rw-r--r-- | poezio/core/handlers.py | 845 |
1 files changed, 267 insertions, 578 deletions
diff --git a/poezio/core/handlers.py b/poezio/core/handlers.py index 620d854c..e92e4aac 100644 --- a/poezio/core/handlers.py +++ b/poezio/core/handlers.py @@ -3,42 +3,41 @@ XMPP-related handlers for the Core class """ import logging -log = logging.getLogger(__name__) 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 import pyasn1_modules.rfc2459 -from slixmpp import InvalidJID, JID, Message +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 pep from poezio import tabs from poezio import xhtml from poezio import multiuserchat as muc -from poezio.common import safeJID +from poezio.common import get_error_message from poezio.config import config, get_image_cache 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, + InfoMessage, + PersistentInfoMessage, +) from poezio.core.commands import dumb_callback @@ -52,6 +51,8 @@ try: except ImportError: PYGMENTS = False +log = logging.getLogger(__name__) + CERT_WARNING_TEXT = """ WARNING: CERTIFICATE FOR %s CHANGED @@ -78,30 +79,27 @@ class HandlerCore: def __init__(self, core): self.core = core - def on_session_start_features(self, _): + async def on_session_start_features(self, _): """ Enable carbons & blocking on session start if wanted and possible """ - - def callback(iq): - if not iq: - return - features = iq['disco_info']['features'] - rostertab = self.core.tabs.by_name_and_class( - 'Roster', tabs.RosterInfoTab) - rostertab.check_blocking(features) - rostertab.check_saslexternal(features) - self.core.check_blocking(features) - if (config.get('enable_carbons') - and 'urn:xmpp:carbons:2' in features): - self.core.xmpp.plugin['xep_0280'].enable() - self.core.check_bookmark_storage(features) - - self.core.xmpp.plugin['xep_0030'].get_info( - jid=self.core.xmpp.boundjid.domain, callback=callback) + iq = await self.core.xmpp.plugin['xep_0030'].get_info( + jid=self.core.xmpp.boundjid.domain + ) + features = iq['disco_info']['features'] + + rostertab = self.core.tabs.by_name_and_class( + 'Roster', tabs.RosterInfoTab) + rostertab.check_saslexternal(features) + rostertab.check_blocking(features) + self.core.check_blocking(features) + if (config.getbool('enable_carbons') + and 'urn:xmpp:carbons:2' in features): + self.core.xmpp.plugin['xep_0280'].enable() + await self.core.check_bookmark_storage(features) def find_identities(self, _): - asyncio.ensure_future( + asyncio.create_task( self.core.xmpp['xep_0030'].get_info_from_domain(), ) @@ -111,7 +109,7 @@ class HandlerCore: """ # first, look for the x (XEP-0045 version 1.28) - if message.xml.find('{http://jabber.org/protocol/muc#user}x') is not None: + if message.match('message/muc'): log.debug('MUC-PM from %s with <x>', with_jid) return True @@ -152,79 +150,64 @@ class HandlerCore: return None - def on_carbon_received(self, message): + async def on_carbon_received(self, message: Message): """ Carbon <received/> received """ - - def ignore_message(recv): - log.debug('%s has category conference, ignoring carbon', - recv['from'].server) - - def receive_message(recv): - recv['to'] = self.core.xmpp.boundjid.full - if recv['receipt']: - return self.on_receipt(recv) - self.on_normal_message(recv) - recv = message['carbon_received'] is_muc_pm = self.is_known_muc_pm(recv, recv['from']) if is_muc_pm: log.debug('%s sent a MUC-PM, ignoring carbon', recv['from']) - return - if is_muc_pm is None: - fixes.has_identity( - self.core.xmpp, + elif is_muc_pm is None: + is_muc = await self.core.xmpp.plugin['xep_0030'].has_identity( recv['from'].bare, - identity='conference', - on_true=functools.partial(ignore_message, recv), - on_false=functools.partial(receive_message, recv)) - return + node='conference', + ) + if is_muc: + log.debug('%s has category conference, ignoring carbon', + recv['from'].server) + else: + recv['to'] = self.core.xmpp.boundjid.full + if recv['receipt']: + await self.on_receipt(recv) + else: + await self.on_normal_message(recv) else: - receive_message(recv) + recv['to'] = self.core.xmpp.boundjid.full + await self.on_normal_message(recv) - def on_carbon_sent(self, message): + async def on_carbon_sent(self, message: Message): """ Carbon <sent/> received """ - - def groupchat_private_message(sent): - self.on_groupchat_private_message(sent, sent=True) - - def send_message(sent): - sent['from'] = self.core.xmpp.boundjid.full - self.on_normal_message(sent) - sent = message['carbon_sent'] is_muc_pm = self.is_known_muc_pm(sent, sent['to']) if is_muc_pm: - groupchat_private_message(sent) - return - if is_muc_pm is None: - fixes.has_identity( - self.core.xmpp, - sent['to'].server, - identity='conference', - on_true=functools.partial(groupchat_private_message, sent), - on_false=functools.partial(send_message, sent)) + await self.on_groupchat_private_message(sent, sent=True) + elif is_muc_pm is None: + is_muc = await self.core.xmpp.plugin['xep_0030'].has_identity( + sent['to'].bare, + node='conference', + ) + if is_muc: + await self.on_groupchat_private_message(sent, sent=True) + else: + sent['from'] = self.core.xmpp.boundjid.full + await self.on_normal_message(sent) else: - send_message(sent) + sent['from'] = self.core.xmpp.boundjid.full + await self.on_normal_message(sent) ### Invites ### - def on_groupchat_invitation(self, message): + async def on_groupchat_invitation(self, message: Message): """ Mediated invitation received """ jid = message['from'] if jid.bare in self.core.pending_invites: return - # there are 2 'x' tags in the messages, making message['x'] useless - invite = StanzaBase( - self.core.xmpp, - xml=message.xml.find( - '{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite' - )) + invite = message['muc']['invite'] # TODO: find out why pylint thinks "inviter" is a list #pylint: disable=no-member inviter = invite['from'] @@ -236,20 +219,23 @@ class HandlerCore: if password: msg += ". The password is \"%s\"." % password self.core.information(msg, 'Info') - if 'invite' in config.get('beep_on').split(): + if 'invite' in config.getstr('beep_on').split(): curses.beep() logger.log_roster_change(inviter.full, 'invited you to %s' % jid.full) self.core.pending_invites[jid.bare] = inviter.full - def on_groupchat_decline(self, decline): + async def on_groupchat_decline(self, decline): "Mediated invitation declined; skip for now" pass - def on_groupchat_direct_invitation(self, message): + async def on_groupchat_direct_invitation(self, message: Message): """ Direct invitation received """ - room = safeJID(message['groupchat_invite']['jid']) + try: + room = JID(message['groupchat_invite']['jid']) + except InvalidJID: + return if room.bare in self.core.pending_invites: return @@ -267,7 +253,7 @@ class HandlerCore: msg += "\nreason: %s" % reason self.core.information(msg, 'Info') - if 'invite' in config.get('beep_on').split(): + if 'invite' in config.getstr('beep_on').split(): curses.beep() self.core.pending_invites[room.bare] = inviter.full @@ -275,32 +261,30 @@ class HandlerCore: ### "classic" messages ### - def on_message(self, message): + async def on_message(self, message: Message): """ When receiving private message from a muc OR a normal message (from one of our contacts) """ - if message.xml.find( - '{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite' - ) is not None: + if message.match('message/muc/invite'): return if message['type'] == 'groupchat': return # Differentiate both type of messages, and call the appropriate handler. if self.is_known_muc_pm(message, message['from']): - self.on_groupchat_private_message(message, sent=False) - return - self.on_normal_message(message) + await self.on_groupchat_private_message(message, sent=False) + else: + await self.on_normal_message(message) - def on_encrypted_message(self, message): + async def on_encrypted_message(self, message: Message): """ When receiving an encrypted message """ if message["body"]: return # Already being handled by on_message. - self.on_message(message) + await self.on_message(message) - def on_error_message(self, message): + async def on_error_message(self, message: Message): """ When receiving any message with type="error" """ @@ -310,7 +294,7 @@ class HandlerCore: if jid_from.full == jid_from.bare: self.core.room_error(message, jid_from.bare) else: - text = self.core.get_error_message(message) + text = get_error_message(message) p_tab = self.core.tabs.by_name_and_class( jid_from.full, tabs.PrivateTab) if p_tab: @@ -319,17 +303,17 @@ class HandlerCore: self.core.information(text, 'Error') return tab = self.core.get_conversation_by_jid(message['from'], create=False) - error_msg = self.core.get_error_message(message, deprecated=True) + error_msg = get_error_message(message, deprecated=True) if not tab: self.core.information(error_msg, 'Error') return error = '\x19%s}%s\x19o' % (dump_tuple(get_theme().COLOR_CHAR_NACK), error_msg) if not tab.nack_message('\n' + error, message['id'], message['to']): - tab.add_message(error, typ=0) + tab.add_message(InfoMessage(error)) self.core.refresh_window() - def on_normal_message(self, message): + async def on_normal_message(self, message: Message): """ When receiving "normal" messages (not a private message from a muc participant) @@ -343,94 +327,36 @@ class HandlerCore: use_xhtml = config.get_by_tabname('enable_xhtml_im', message['from'].bare) tmp_dir = get_image_cache() - body = xhtml.get_body_from_message_stanza( - message, use_xhtml=use_xhtml, extract_images_to=tmp_dir) - if not body: + if not xhtml.get_body_from_message_stanza( + message, use_xhtml=use_xhtml, extract_images_to=tmp_dir): if not self.core.xmpp.plugin['xep_0380'].has_eme(message): return self.core.xmpp.plugin['xep_0380'].replace_body_with_eme(message) - body = message['body'] - remote_nick = '' # 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 - # check for a name - if conv_jid.bare in roster: - remote_nick = roster[conv_jid.bare].name - # check for a received nick - if not remote_nick and config.get('enable_user_nick'): - if message.xml.find( - '{http://jabber.org/protocol/nick}nick') is not None: - remote_nick = message['nick']['nick'] - if not remote_nick: - remote_nick = conv_jid.user - if not remote_nick: - remote_nick = conv_jid.full own = False # 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 own = True # we are not part of that message, drop it else: return - conversation = self.core.get_conversation_by_jid(conv_jid, create=True) - if isinstance(conversation, - tabs.DynamicConversationTab) and conv_jid.resource: - conversation.lock(conv_jid.resource) - - if not own and not conversation.nick: - conversation.nick = remote_nick - elif not own: - remote_nick = conversation.get_nick() - - if not own: - conversation.last_remote_message = datetime.now() - - self.core.events.trigger('conversation_msg', message, conversation) - 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) - - def try_modify(): - if message.xml.find('{urn:xmpp:message-correct:0}replace') is None: - return False - replaced_id = message['replace']['id'] - if replaced_id and config.get_by_tabname('group_corrections', - conv_jid.bare): - try: - conversation.modify_message( - body, - replaced_id, - message['id'], - jid=jid, - nickname=remote_nick) - return True - except CorrectionError: - log.debug('Unable to correct a message', exc_info=True) - return False + conversation = self.core.get_conversation_by_jid(conv_jid, create=False) + if conversation is None: + conversation = tabs.DynamicConversationTab( + self.core, + JID(conv_jid.bare), + initial=message, + ) + self.core.tabs.append(conversation) + else: + await conversation.handle_message(message) - if not try_modify(): - conversation.add_message( - body, - date, - nickname=remote_nick, - nick_color=color, - history=delayed, - identifier=message['id'], - jid=jid, - typ=1) - - if not own and 'private' in config.get('beep_on').split(): + if not own and 'private' in config.getstr('beep_on').split(): if not config.get_by_tabname('disable_beep', conv_jid.bare): curses.beep() if self.core.tabs.current_tab is not conversation: @@ -443,7 +369,7 @@ class HandlerCore: else: self.core.refresh_window() - async def on_0084_avatar(self, msg): + async def on_0084_avatar(self, msg: Message): jid = msg['from'].bare contact = roster[jid] if not contact: @@ -493,7 +419,7 @@ class HandlerCore: exc_info=True) return - async def on_vcard_avatar(self, pres): + async def on_vcard_avatar(self, pres: Presence): jid = pres['from'].bare contact = roster[jid] if not contact: @@ -529,7 +455,7 @@ class HandlerCore: log.debug( 'Failed writing %s’s avatar to cache:', jid, exc_info=True) - def on_nick_received(self, message): + async def on_nick_received(self, message: Message): """ Called when a pep notification for a user nickname is received @@ -543,172 +469,7 @@ class HandlerCore: else: contact.name = '' - def on_gaming_event(self, message): - """ - Called when a pep notification for user gaming - is received - """ - contact = roster[message['from'].bare] - if not contact: - return - item = message['pubsub_event']['items']['item'] - old_gaming = contact.gaming - if item.xml.find('{urn:xmpp:gaming:0}gaming') is not None: - item = item['gaming'] - # only name and server_address are used for now - contact.gaming = { - 'character_name': item['character_name'], - 'character_profile': item['character_profile'], - 'name': item['name'], - 'level': item['level'], - 'uri': item['uri'], - 'server_name': item['server_name'], - 'server_address': item['server_address'], - } - else: - contact.gaming = {} - - if contact.gaming: - logger.log_roster_change( - contact.bare_jid, 'is playing %s' % - (common.format_gaming_string(contact.gaming))) - - if old_gaming != contact.gaming and config.get_by_tabname( - 'display_gaming_notifications', contact.bare_jid): - if contact.gaming: - self.core.information( - '%s is playing %s' % (contact.bare_jid, - common.format_gaming_string( - contact.gaming)), 'Gaming') - else: - self.core.information(contact.bare_jid + ' stopped playing.', - 'Gaming') - - def on_mood_event(self, message): - """ - Called when a pep notification for a user mood - is received. - """ - contact = roster[message['from'].bare] - if not contact: - return - roster.modified() - item = message['pubsub_event']['items']['item'] - old_mood = contact.mood - if item.xml.find('{http://jabber.org/protocol/mood}mood') is not None: - mood = item['mood']['value'] - if mood: - mood = pep.MOODS.get(mood, mood) - text = item['mood']['text'] - if text: - mood = '%s (%s)' % (mood, text) - contact.mood = mood - else: - contact.mood = '' - else: - contact.mood = '' - - if contact.mood: - logger.log_roster_change(contact.bare_jid, - 'has now the mood: %s' % contact.mood) - - if old_mood != contact.mood and config.get_by_tabname( - 'display_mood_notifications', contact.bare_jid): - if contact.mood: - self.core.information( - 'Mood from ' + contact.bare_jid + ': ' + contact.mood, - 'Mood') - else: - self.core.information( - contact.bare_jid + ' stopped having their mood.', 'Mood') - - def on_activity_event(self, message): - """ - Called when a pep notification for a user activity - is received. - """ - contact = roster[message['from'].bare] - if not contact: - return - roster.modified() - item = message['pubsub_event']['items']['item'] - old_activity = contact.activity - if item.xml.find( - '{http://jabber.org/protocol/activity}activity') is not None: - try: - activity = item['activity']['value'] - except ValueError: - return - if activity[0]: - general = pep.ACTIVITIES.get(activity[0]) - s = general['category'] - if activity[1]: - s = s + '/' + general.get(activity[1], 'other') - text = item['activity']['text'] - if text: - s = '%s (%s)' % (s, text) - contact.activity = s - else: - contact.activity = '' - else: - contact.activity = '' - - if contact.activity: - logger.log_roster_change( - contact.bare_jid, 'has now the activity %s' % contact.activity) - - if old_activity != contact.activity and config.get_by_tabname( - 'display_activity_notifications', contact.bare_jid): - if contact.activity: - self.core.information( - 'Activity from ' + contact.bare_jid + ': ' + - contact.activity, 'Activity') - else: - self.core.information( - contact.bare_jid + ' stopped doing their activity.', - 'Activity') - - def on_tune_event(self, message): - """ - Called when a pep notification for a user tune - is received - """ - contact = roster[message['from'].bare] - if not contact: - return - roster.modified() - item = message['pubsub_event']['items']['item'] - old_tune = contact.tune - if item.xml.find('{http://jabber.org/protocol/tune}tune') is not None: - item = item['tune'] - contact.tune = { - 'artist': item['artist'], - 'length': item['length'], - 'rating': item['rating'], - 'source': item['source'], - 'title': item['title'], - 'track': item['track'], - 'uri': item['uri'] - } - else: - contact.tune = {} - - if contact.tune: - logger.log_roster_change( - message['from'].bare, 'is now listening to %s' % - common.format_tune_string(contact.tune)) - - if old_tune != contact.tune and config.get_by_tabname( - 'display_tune_notifications', contact.bare_jid): - if contact.tune: - self.core.information( - 'Tune from ' + message['from'].bare + ': ' + - common.format_tune_string(contact.tune), 'Tune') - else: - self.core.information( - contact.bare_jid + ' stopped listening to music.', 'Tune') - - def on_groupchat_message(self, message): + async def on_groupchat_message(self, message: Message) -> None: """ Triggered whenever a message is received from a multi-user chat room. """ @@ -725,88 +486,33 @@ 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 - - self.core.events.trigger('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) - if not body: - return - - old_state = tab.state - delayed, date = common.find_delayed_tag(message) - 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, - nickname=nick_from, - user=user): - self.core.events.trigger('highlight', message, tab) - replaced = True - except CorrectionError: - log.debug('Unable to correct a message', exc_info=True) - if not replaced and tab.add_message( - body, - date, - nick_from, - history=delayed, - identifier=message['id'], - jid=message['from'], - typ=1): - self.core.events.trigger('highlight', message, tab) - - if message['from'].resource == tab.own_nick: - tab.last_sent_message = message - - 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.get('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() - def on_muc_own_nickchange(self, muc): + def on_muc_own_nickchange(self, muc: tabs.MucTab): "We changed our nick in a MUC" for tab in self.core.get_tabs(tabs.PrivateTab): if tab.parent_muc == muc: tab.own_nick = muc.own_nick - def on_groupchat_private_message(self, message, sent): + async def on_groupchat_private_message(self, message: Message, sent: bool): """ We received a Private Message (from someone in a Muc) """ jid = message['to'] if sent else message['from'] with_nick = jid.resource if not with_nick: - self.on_groupchat_message(message) + await self.on_groupchat_message(message) return room_from = jid.bare - use_xhtml = config.get_by_tabname('enable_xhtml_im', jid.bare) + use_xhtml = config.get_by_tabname( + 'enable_xhtml_im', + jid.bare + ) tmp_dir = get_image_cache() body = xhtml.get_body_from_message_stanza( message, use_xhtml=use_xhtml, extract_images_to=tmp_dir) @@ -814,59 +520,27 @@ class HandlerCore: jid.full, tabs.PrivateTab) # get the tab with the private conversation ignore = config.get_by_tabname('ignore_private', room_from) - if not tab: # It's the first message we receive: create the tab - if body and not ignore: - tab = self.core.open_private_window(room_from, with_nick, - False) - # Tab can still be None here, when receiving carbons of a MUC-PM for - # example - sender_nick = (tab and tab.own_nick - or self.core.own_nick) if sent else with_nick if ignore and not sent: - self.core.events.trigger('ignored_private', message, tab) + await self.core.events.trigger_async('ignored_private', message, tab) msg = config.get_by_tabname('private_auto_response', room_from) if msg and body: self.core.xmpp.send_message( mto=jid.full, mbody=msg, mtype='chat') return - self.core.events.trigger('private_msg', message, tab) - body = xhtml.get_body_from_message_stanza( - message, use_xhtml=use_xhtml, extract_images_to=tmp_dir) - if not body or not tab: - return - replaced = False - user = tab.parent_muc.get_user_by_name(with_nick) - 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', room_from): - try: - tab.modify_message( - body, - replaced_id, - message['id'], - user=user, - jid=message['from'], - nickname=sender_nick) - replaced = True - except CorrectionError: - log.debug('Unable to correct a message', exc_info=True) - if not replaced: - tab.add_message( - body, - time=None, - nickname=sender_nick, - nick_color=get_theme().COLOR_OWN_NICK if sent else None, - forced_user=user, - identifier=message['id'], - jid=message['from'], - typ=1) - if sent: - tab.last_sent_message = message + if tab is None: # It's the first message we receive: create the tab + if body and not ignore: + tab = tabs.PrivateTab( + self.core, + jid, + self.core.own_nick, + initial=message, + ) + self.core.tabs.append(tab) + tab.parent_muc.privates.append(tab) else: - tab.last_remote_message = datetime.now() + await tab.handle_message(message) - if not sent and 'private' in config.get('beep_on').split(): + if not sent and 'private' in config.getstr('beep_on').split(): if not config.get_by_tabname('disable_beep', jid.full): curses.beep() if tab is self.core.tabs.current_tab: @@ -877,37 +551,37 @@ class HandlerCore: ### Chatstates ### - def on_chatstate_active(self, message): - self._on_chatstate(message, "active") + async def on_chatstate_active(self, message: Message): + await self._on_chatstate(message, "active") - def on_chatstate_inactive(self, message): - self._on_chatstate(message, "inactive") + async def on_chatstate_inactive(self, message: Message): + await self._on_chatstate(message, "inactive") - def on_chatstate_composing(self, message): - self._on_chatstate(message, "composing") + async def on_chatstate_composing(self, message: Message): + await self._on_chatstate(message, "composing") - def on_chatstate_paused(self, message): - self._on_chatstate(message, "paused") + async def on_chatstate_paused(self, message: Message): + await self._on_chatstate(message, "paused") - def on_chatstate_gone(self, message): - self._on_chatstate(message, "gone") + async def on_chatstate_gone(self, message: Message): + await self._on_chatstate(message, "gone") - def _on_chatstate(self, message, state): + async def _on_chatstate(self, message: Message, state: str): if message['type'] == 'chat': - if not self._on_chatstate_normal_conversation(message, state): + if not await self._on_chatstate_normal_conversation(message, state): tab = self.core.tabs.by_name_and_class(message['from'].full, tabs.PrivateTab) if not tab: return - self._on_chatstate_private_conversation(message, state) + await self._on_chatstate_private_conversation(message, state) elif message['type'] == 'groupchat': - self.on_chatstate_groupchat_conversation(message, state) + await self.on_chatstate_groupchat_conversation(message, state) - def _on_chatstate_normal_conversation(self, message, state): + async def _on_chatstate_normal_conversation(self, message: Message, state: str): tab = self.core.get_conversation_by_jid(message['from'], False) if not tab: return False - self.core.events.trigger('normal_chatstate', message, tab) + await self.core.events.trigger_async('normal_chatstate', message, tab) tab.chatstate = state if state == 'gone' and isinstance(tab, tabs.DynamicConversationTab): tab.unlock() @@ -919,7 +593,7 @@ class HandlerCore: self.core.refresh_tab_win() return True - def _on_chatstate_private_conversation(self, message, state): + async def _on_chatstate_private_conversation(self, message: Message, state: str): """ Chatstate received in a private conversation from a MUC """ @@ -927,7 +601,7 @@ class HandlerCore: tabs.PrivateTab) if not tab: return - self.core.events.trigger('private_chatstate', message, tab) + await self.core.events.trigger_async('private_chatstate', message, tab) tab.chatstate = state if tab == self.core.tabs.current_tab: tab.refresh_info_header() @@ -936,7 +610,7 @@ class HandlerCore: _composing_tab_state(tab, state) self.core.refresh_tab_win() - def on_chatstate_groupchat_conversation(self, message, state): + async def on_chatstate_groupchat_conversation(self, message: Message, state: str): """ Chatstate received in a MUC """ @@ -944,7 +618,7 @@ class HandlerCore: room_from = message.get_mucroom() tab = self.core.tabs.by_name_and_class(room_from, tabs.MucTab) if tab and tab.get_user_by_name(nick): - self.core.events.trigger('muc_chatstate', message, tab) + await self.core.events.trigger_async('muc_chatstate', message, tab) tab.get_user_by_name(nick).chatstate = state if tab == self.core.tabs.current_tab: if not self.core.size.tab_degrade_x: @@ -962,7 +636,7 @@ class HandlerCore: return '%s: %s' % (error_condition, error_text) if error_text else error_condition - def on_version_result(self, iq): + def on_version_result(self, iq: Iq): """ Handle the result of a /version command. """ @@ -979,7 +653,7 @@ class HandlerCore: 'an unknown platform')) self.core.information(version, 'Info') - def on_bookmark_result(self, iq): + def on_bookmark_result(self, iq: Iq): """ Handle the result of a /bookmark commands. """ @@ -991,7 +665,7 @@ class HandlerCore: ### subscription-related handlers ### - def on_roster_update(self, iq): + async def on_roster_update(self, iq: Iq): """ The roster was received. """ @@ -1010,7 +684,7 @@ class HandlerCore: if isinstance(self.core.tabs.current_tab, tabs.RosterInfoTab): self.core.refresh_window() - def on_subscription_request(self, presence): + async def on_subscription_request(self, presence: Presence): """subscribe received""" jid = presence['from'].bare contact = roster[jid] @@ -1033,7 +707,7 @@ class HandlerCore: if isinstance(self.core.tabs.current_tab, tabs.RosterInfoTab): self.core.refresh_window() - def on_subscription_authorized(self, presence): + async def on_subscription_authorized(self, presence: Presence): """subscribed received""" jid = presence['from'].bare contact = roster[jid] @@ -1048,7 +722,7 @@ class HandlerCore: if isinstance(self.core.tabs.current_tab, tabs.RosterInfoTab): self.core.refresh_window() - def on_subscription_remove(self, presence): + async def on_subscription_remove(self, presence: Presence): """unsubscribe received""" jid = presence['from'].bare contact = roster[jid] @@ -1061,7 +735,7 @@ class HandlerCore: if isinstance(self.core.tabs.current_tab, tabs.RosterInfoTab): self.core.refresh_window() - def on_subscription_removed(self, presence): + async def on_subscription_removed(self, presence: Presence): """unsubscribed received""" jid = presence['from'].bare contact = roster[jid] @@ -1082,9 +756,8 @@ class HandlerCore: ### Presence-related handlers ### - def on_presence(self, presence): - if presence.match('presence/muc') or presence.xml.find( - '{http://jabber.org/protocol/muc#user}x') is not None: + async def on_presence(self, presence: Presence): + if presence.match('presence/muc'): return jid = presence['from'] contact = roster[jid.bare] @@ -1098,8 +771,8 @@ class HandlerCore: return roster.modified() contact.error = None - self.core.events.trigger('normal_presence', presence, - contact[jid.full]) + await self.core.events.trigger_async('normal_presence', presence, + contact[jid.full]) tab = self.core.get_conversation_by_jid(jid, create=False) if tab: tab.update_status( @@ -1110,21 +783,20 @@ class HandlerCore: tab.refresh() self.core.doupdate() - def on_presence_error(self, presence): + async def on_presence_error(self, presence: Presence): jid = presence['from'] contact = roster[jid.bare] if not contact: return roster.modified() - contact.error = presence['error']['type'] + ': ' + presence['error']['condition'] + contact.error = presence['error']['text'] or presence['error']['type'] + ': ' + presence['error']['condition'] # TODO: reset chat states status on presence error - def on_got_offline(self, presence): + async def on_got_offline(self, presence: Presence): """ A JID got offline """ - if presence.match('presence/muc') or presence.xml.find( - '{http://jabber.org/protocol/muc#user}x') is not None: + if presence.match('presence/muc'): return jid = presence['from'] status = presence['status'] @@ -1152,12 +824,11 @@ class HandlerCore: if isinstance(self.core.tabs.current_tab, tabs.RosterInfoTab): self.core.refresh_window() - def on_got_online(self, presence): + async def on_got_online(self, presence: Presence): """ A JID got online """ - if presence.match('presence/muc') or presence.xml.find( - '{http://jabber.org/protocol/muc#user}x') is not None: + if presence.match('presence/muc'): return jid = presence['from'] contact = roster[jid.bare] @@ -1174,7 +845,7 @@ class HandlerCore: 'status': presence['status'], 'show': presence['show'], }) - self.core.events.trigger('normal_presence', presence, resource) + await self.core.events.trigger_async('normal_presence', presence, resource) name = contact.name if contact.name else jid.bare self.core.add_information_message_to_conversation_tab( jid.full, '\x195}%s is \x194}online' % name) @@ -1192,7 +863,7 @@ class HandlerCore: if isinstance(self.core.tabs.current_tab, tabs.RosterInfoTab): self.core.refresh_window() - def on_groupchat_presence(self, presence): + async def on_groupchat_presence(self, presence: Presence): """ Triggered whenever a presence stanza is received from a user in a multi-user chat room. Display the presence on the room window and update the @@ -1201,19 +872,19 @@ class HandlerCore: from_room = presence['from'].bare tab = self.core.tabs.by_name_and_class(from_room, tabs.MucTab) if tab: - self.core.events.trigger('muc_presence', presence, tab) + await self.core.events.trigger_async('muc_presence', presence, tab) tab.handle_presence(presence) ### Connection-related handlers ### - def on_failed_connection(self, error): + async def on_failed_connection(self, error: str): """ We cannot contact the remote server """ self.core.information( "Connection to remote server failed: %s" % (error, ), 'Error') - def on_session_end(self, event): + async def on_session_end(self, event): """ Called when a session is terminated (e.g. due to a manual disconnect or a 0198 resume fail) """ @@ -1222,7 +893,7 @@ class HandlerCore: for tab in self.core.get_tabs(tabs.MucTab): tab.disconnect() - def on_session_resumed(self, event): + async def on_session_resumed(self, event): """ Called when a session is successfully resumed by 0198 """ @@ -1233,22 +904,23 @@ class HandlerCore: """ When we are disconnected from remote server """ - if 'disconnect' in config.get('beep_on').split(): + if 'disconnect' in config.getstr('beep_on').split(): curses.beep() # Stop the ping plugin. It would try to send stanza on regular basis self.core.xmpp.plugin['xep_0199'].disable_keepalive() msg_typ = 'Error' if not self.core.legitimate_disconnect else 'Info' self.core.information("Disconnected from server%s." % (event and ": %s" % event or ""), msg_typ) - if self.core.legitimate_disconnect or not config.get( - 'auto_reconnect', True): + if self.core.legitimate_disconnect or not config.getbool( + 'auto_reconnect'): return if (self.core.last_stream_error and self.core.last_stream_error[1]['condition'] in ( 'conflict', 'host-unknown')): return await asyncio.sleep(1) - self.core.information("Auto-reconnecting.", 'Info') - self.core.xmpp.start() + if not self.core.xmpp.is_connecting() and not self.core.xmpp.is_connected(): + self.core.information("Auto-reconnecting.", 'Info') + self.core.xmpp.start() async def on_reconnect_delay(self, event): """ @@ -1256,7 +928,7 @@ class HandlerCore: """ self.core.information("Reconnecting in %d seconds..." % (event), 'Info') - def on_stream_error(self, event): + async def on_stream_error(self, event): """ When we receive a stream error """ @@ -1265,7 +937,7 @@ class HandlerCore: if event: self.core.last_stream_error = (time.time(), event) - def on_failed_all_auth(self, event): + async def on_failed_all_auth(self, event): """ Authentication failed """ @@ -1273,7 +945,7 @@ class HandlerCore: 'Error') self.core.legitimate_disconnect = True - def on_no_auth(self, event): + async def on_no_auth(self, event): """ Authentication failed (no mech) """ @@ -1281,14 +953,14 @@ class HandlerCore: "Authentication failed, no login method available.", 'Error') self.core.legitimate_disconnect = True - def on_connected(self, event): + async def on_connected(self, event): """ Remote host responded, but we are not yet authenticated """ self.core.information("Connected to server.", 'Info') self.core.legitimate_disconnect = False - def on_session_start(self, event): + async def on_session_start(self, event): """ Called when we are connected and authenticated """ @@ -1303,26 +975,26 @@ class HandlerCore: self.core.xmpp.get_roster() roster.update_contact_groups(self.core.xmpp.boundjid.bare) # send initial presence - if config.get('send_initial_presence'): + if config.getbool('send_initial_presence'): pres = self.core.xmpp.make_presence() pres['show'] = self.core.status.show pres['status'] = self.core.status.message - self.core.events.trigger('send_normal_presence', pres) + await self.core.events.trigger_async('send_normal_presence', pres) pres.send() self.core.bookmarks.get_local() # join all the available bookmarks. As of yet, this is just the local ones - self.core.join_initial_rooms(self.core.bookmarks) + self.core.join_initial_rooms(self.core.bookmarks.local()) - if config.get('enable_user_nick'): + if config.getbool('enable_user_nick'): self.core.xmpp.plugin['xep_0172'].publish_nick( nick=self.core.own_nick, callback=dumb_callback) - asyncio.ensure_future(self.core.xmpp.plugin['xep_0115'].update_caps()) + asyncio.create_task(self.core.xmpp.plugin['xep_0115'].update_caps()) # Start the ping's plugin regular event self.core.xmpp.set_keepalive_values() ### Other handlers ### - def on_status_codes(self, message): + async def on_status_codes(self, message: Message): """ Handle groupchat messages with status codes. Those are received when a room configuration change occurs. @@ -1351,41 +1023,57 @@ class HandlerCore: if show_unavailable or hide_unavailable or non_priv or logging_off\ or non_anon or semi_anon or full_anon: tab.add_message( - '\x19%(info_col)s}Info: A configuration change not privacy-related occurred.' % info_col, - typ=2) + PersistentInfoMessage( + 'Info: A configuration change not privacy-related occurred.' + ), + ) modif = True if show_unavailable: tab.add_message( - '\x19%(info_col)s}Info: The unavailable members are now shown.' % info_col, - typ=2) + PersistentInfoMessage( + 'Info: The unavailable members are now shown.' + ), + ) elif hide_unavailable: tab.add_message( - '\x19%(info_col)s}Info: The unavailable members are now hidden.' % info_col, - typ=2) + PersistentInfoMessage( + 'Info: The unavailable members are now hidden.', + ), + ) if non_anon: tab.add_message( - '\x191}Warning:\x19%(info_col)s} The room is now not anonymous. (public JID)' % info_col, - typ=2) + PersistentInfoMessage( + '\x191}Warning:\x19%(info_col)s} The room is now not anonymous. (public JID)' % info_col + ), + ) elif semi_anon: tab.add_message( - '\x19%(info_col)s}Info: The room is now semi-anonymous. (moderators-only JID)' % info_col, - typ=2) + PersistentInfoMessage( + 'Info: The room is now semi-anonymous. (moderators-only JID)', + ), + ) elif full_anon: tab.add_message( - '\x19%(info_col)s}Info: The room is now fully anonymous.' % info_col, - typ=2) + PersistentInfoMessage( + 'Info: The room is now fully anonymous.', + ), + ) if logging_on: tab.add_message( - '\x191}Warning: \x19%(info_col)s}This room is publicly logged' % info_col, - typ=2) + PersistentInfoMessage( + '\x191}Warning: \x19%(info_col)s}This room is publicly logged' % info_col + ), + ) elif logging_off: tab.add_message( - '\x19%(info_col)s}Info: This room is not logged anymore.' % info_col, - typ=2) + PersistentInfoMessage( + 'Info: This room is not logged anymore.', + ), + ) if modif: self.core.refresh_window() - def on_groupchat_subject(self, message): + async def on_groupchat_subject(self, message: Message): """ Triggered when the topic is changed. """ @@ -1424,23 +1112,25 @@ class HandlerCore: if nick_from: tab.add_message( - "%(user)s set the subject to: \x19%(text_col)s}%(subject)s" - % fmt, - str_time=time, - typ=2) + PersistentInfoMessage( + "%(user)s set the subject to: \x19%(text_col)s}%(subject)s" % fmt, + time=time, + ), + ) else: tab.add_message( - "\x19%(info_col)s}The subject is: \x19%(text_col)s}%(subject)s" - % fmt, - str_time=time, - typ=2) + PersistentInfoMessage( + "The subject is: \x19%(text_col)s}%(subject)s" % fmt, + time=time, + ), + ) tab.topic = subject tab.topic_from = nick_from if self.core.tabs.by_name_and_class( room_from, tabs.MucTab) is self.core.tabs.current_tab: self.core.refresh_window() - def on_receipt(self, message): + async def on_receipt(self, message): """ When a delivery receipt is received (XEP-0184) """ @@ -1462,57 +1152,54 @@ class HandlerCore: except AckError: log.debug('Error while receiving an ack', exc_info=True) - def on_data_form(self, message): + async def on_data_form(self, message: Message): """ When a data form is received """ self.core.information(str(message)) - def on_attention(self, message): + async def on_attention(self, message: Message): """ Attention probe received. """ jid_from = message['from'] self.core.information('%s requests your attention!' % jid_from, 'Info') - for tab in self.core.tabs: - if tab.jid == jid_from: - tab.state = 'attention' - self.core.refresh_tab_win() - return - for tab in self.core.tabs: - if tab.jid.bare == jid_from.bare: - tab.state = 'attention' - self.core.refresh_tab_win() - return - self.core.information('%s tab not found.' % jid_from, 'Error') + tab = ( + self.core.tabs.by_name_and_class( + jid_from.full, tabs.ChatTab + ) or self.core.tabs.by_name_and_class( + jid_from.bare, tabs.ChatTab + ) + ) + if tab and tab is not self.core.tabs.current_tab: + tab.state = "attention" + self.core.refresh_tab_win() - def outgoing_stanza(self, stanza): + def outgoing_stanza(self, stanza: StanzaBase): """ We are sending a new stanza, write it in the xml buffer if needed. """ if self.core.xml_tab: + stanza_str = str(stanza) if PYGMENTS: - xhtml_text = highlight(str(stanza), LEXER, FORMATTER) + xhtml_text = highlight(stanza_str, LEXER, FORMATTER) poezio_colored = xhtml.xhtml_to_poezio_colors( xhtml_text, force=True).rstrip('\x19o').strip() else: - poezio_colored = str(stanza) - char = get_theme().CHAR_XML_OUT - self.core.add_message_to_text_buffer( - self.core.xml_buffer, - poezio_colored, - nickname=char) + poezio_colored = stanza_str + self.core.xml_buffer.add_message( + XMLLog(txt=poezio_colored, incoming=False), + ) try: if self.core.xml_tab.match_stanza( - ElementBase(ET.fromstring(stanza))): - self.core.add_message_to_text_buffer( - self.core.xml_tab.filtered_buffer, - poezio_colored, - nickname=char) + ElementBase(ET.fromstring(stanza_str))): + self.core.xml_tab.filtered_buffer.add_message( + XMLLog(txt=poezio_colored, incoming=False), + ) except: # Most of the time what gets logged is whitespace pings. Skip. # And also skip tab updates. - if stanza.strip() != '': + if stanza_str.strip() == '': return None log.debug('', exc_info=True) @@ -1520,7 +1207,7 @@ class HandlerCore: self.core.tabs.current_tab.refresh() self.core.doupdate() - def incoming_stanza(self, stanza): + def incoming_stanza(self, stanza: StanzaBase): """ We are receiving a new stanza, write it in the xml buffer if needed. """ @@ -1531,17 +1218,14 @@ class HandlerCore: xhtml_text, force=True).rstrip('\x19o').strip() else: poezio_colored = str(stanza) - char = get_theme().CHAR_XML_IN - self.core.add_message_to_text_buffer( - self.core.xml_buffer, - poezio_colored, - nickname=char) + self.core.xml_buffer.add_message( + XMLLog(txt=poezio_colored, incoming=True), + ) try: if self.core.xml_tab.match_stanza(stanza): - self.core.add_message_to_text_buffer( - self.core.xml_tab.filtered_buffer, - poezio_colored, - nickname=char) + self.core.xml_tab.filtered_buffer.add_message( + XMLLog(txt=poezio_colored, incoming=True), + ) except: log.debug('', exc_info=True) if isinstance(self.core.tabs.current_tab, tabs.XMLTab): @@ -1580,19 +1264,24 @@ class HandlerCore: self.core.add_tab(confirm_tab, True) self.core.doupdate() + # handle resize + prev_value = signal.signal(signal.SIGWINCH, self.core.sigwinch_handler) while not confirm_tab.done: - sel = select.select([sys.stdin], [], [], 5)[0] - - if sel: - self.core.on_input_readable() + try: + sel = select.select([sys.stdin], [], [], 0.5)[0] + if sel: + self.core.on_input_readable() + except: + continue + signal.signal(signal.SIGWINCH, prev_value) def validate_ssl(self, pem): """ Check the server certificate using the slixmpp ssl_cert event """ - if config.get('ignore_certificate'): + if config.getbool('ignore_certificate'): return - cert = config.get('certificate') + cert = config.getstr('certificate') # update the cert representation when it uses the old one if cert and ':' not in cert: cert = ':'.join( @@ -1701,7 +1390,7 @@ class HandlerCore: def adhoc_error(self, iq, adhoc_session): self.core.xmpp.plugin['xep_0050'].terminate_command(adhoc_session) - error_message = self.core.get_error_message(iq) + error_message = get_error_message(iq) self.core.information( "An error occurred while executing the command: %s" % (error_message), 'Error') @@ -1734,7 +1423,7 @@ def _composing_tab_state(tab, state): else: return # should not happen - show = config.get('show_composing_tabs') + show = config.getstr('show_composing_tabs').lower() show = show in values if tab.state != 'composing' and state == 'composing': |