handlers.py 68.2 KB
Newer Older
mathieui's avatar
mathieui committed
1 2 3 4 5 6 7
"""
XMPP-related handlers for the Core class
"""

import logging
log = logging.getLogger(__name__)

8 9
from typing import Optional

mathieui's avatar
mathieui committed
10
import asyncio
mathieui's avatar
mathieui committed
11
import curses
mathieui's avatar
mathieui committed
12
import functools
mathieui's avatar
mathieui committed
13
import select
mathieui's avatar
mathieui committed
14
import ssl
mathieui's avatar
mathieui committed
15
import sys
mathieui's avatar
mathieui committed
16
import time
17
from datetime import datetime
18
from hashlib import sha1, sha256, sha512
Link Mauve's avatar
Link Mauve committed
19
from os import path
mathieui's avatar
mathieui committed
20

21 22 23
import pyasn1.codec.der.decoder
import pyasn1.codec.der.encoder
import pyasn1_modules.rfc2459
24
from slixmpp import InvalidJID, JID, Message
25 26
from slixmpp.xmlstream.stanzabase import StanzaBase, ElementBase
from xml.etree import ElementTree as ET
mathieui's avatar
mathieui committed
27

28 29 30 31 32 33 34
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
35
from poezio.config import config, get_image_cache
36
from poezio.core.structs import Status
37 38 39 40 41
from poezio.contact import Resource
from poezio.logger import logger
from poezio.roster import roster
from poezio.text_buffer import CorrectionError, AckError
from poezio.theming import dump_tuple, get_theme
42
from poezio.ui.types import XMLLog, Message as PMessage, BaseMessage, InfoMessage
mathieui's avatar
mathieui committed
43

mathieui's avatar
mathieui committed
44
from poezio.core.commands import dumb_callback
mathieui's avatar
mathieui committed
45

46 47 48 49 50 51
try:
    from pygments import highlight
    from pygments.lexers import get_lexer_by_name
    from pygments.formatters import HtmlFormatter
    LEXER = get_lexer_by_name('xml')
    FORMATTER = HtmlFormatter(noclasses=True)
52
    PYGMENTS = True
53
except ImportError:
54
    PYGMENTS = False
55

56 57 58 59 60 61 62
CERT_WARNING_TEXT = """
WARNING: CERTIFICATE FOR %s CHANGED

This can be part of a normal renewal process, but can also mean that \
an attacker is performing a man-in-the-middle attack on your connection.
When in doubt, check with your administrator using another channel.

63
SHA-256 of the old certificate (SPKI): %s
64

65
SHA-256 of the new certificate (SPKI): %s
66 67 68 69 70 71 72 73 74 75 76
"""

HTTP_VERIF_TEXT = """
Someone (maybe you) has requested an identity verification
using method "%s" for the url "%s".

The transaction id is: %s
And the XMPP address of the verification service is %s.

"""

77 78 79 80 81 82 83 84 85

class HandlerCore:
    def __init__(self, core):
        self.core = core

    def on_session_start_features(self, _):
        """
        Enable carbons & blocking on session start if wanted and possible
        """
mathieui's avatar
mathieui committed
86

87 88 89 90
        def callback(iq):
            if not iq:
                return
            features = iq['disco_info']['features']
91 92
            rostertab = self.core.tabs.by_name_and_class(
                'Roster', tabs.RosterInfoTab)
93 94
            rostertab.check_blocking(features)
            rostertab.check_saslexternal(features)
95
            self.core.check_blocking(features)
mathieui's avatar
mathieui committed
96 97
            if (config.get('enable_carbons')
                    and 'urn:xmpp:carbons:2' in features):
98 99
                self.core.xmpp.plugin['xep_0280'].enable()
            self.core.check_bookmark_storage(features)
100

mathieui's avatar
mathieui committed
101 102
        self.core.xmpp.plugin['xep_0030'].get_info(
            jid=self.core.xmpp.boundjid.domain, callback=callback)
103

104 105 106 107 108
    def find_identities(self, _):
        asyncio.ensure_future(
            self.core.xmpp['xep_0030'].get_info_from_domain(),
        )

109
    def is_known_muc_pm(self, message: Message, with_jid: JID) -> Optional[bool]:
110 111 112
        """
        Try to determine whether a given message is a MUC-PM, without a roundtrip. Returns None when it's not clear
        """
113

114
        # first, look for the x (XEP-0045 version 1.28)
115 116
        if message.xml.find('{http://jabber.org/protocol/muc#user}x') is not None:
            log.debug('MUC-PM from %s with <x>', with_jid)
117
            return True
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143

        jid_bare = with_jid.bare

        # then, look whether we have a matching tab with barejid
        tab = self.core.tabs.by_jid(JID(jid_bare))
        if tab is not None:
            if isinstance(tab, tabs.MucTab):
                log.debug('MUC-PM from %s in known MucTab', with_jid)
                return True
            one_to_one = isinstance(tab, (
                tabs.ConversationTab,
                tabs.DynamicConversationTab,
            ))
            if one_to_one:
                return False

        # then, look whether we have a matching tab with fulljid
        if with_jid.resource:
            tab = self.core.tabs.by_jid(with_jid)
            if tab is not None:
                if isinstance(tab, tabs.PrivateTab):
                    log.debug('MUC-PM from %s in known PrivateTab', with_jid)
                    return True
                if isinstance(tab, tabs.StaticConversationTab):
                    return False

144
        # then, look in the roster
145
        if jid_bare in roster and roster[jid_bare].subscription != 'none':
146
            return False
147

148
        # then, check bookmarks
149 150 151 152
        for bm in self.core.bookmarks:
            if bm.jid.bare == jid_bare:
                log.debug('MUC-PM from %s in bookmarks', with_jid)
                return True
153

154 155
        return None

156 157 158 159
    def on_carbon_received(self, message):
        """
        Carbon <received/> received
        """
mathieui's avatar
mathieui committed
160

161 162 163
        def ignore_message(recv):
            log.debug('%s has category conference, ignoring carbon',
                      recv['from'].server)
mathieui's avatar
mathieui committed
164

165
        def receive_message(recv):
166
            recv['to'] = self.core.xmpp.boundjid.full
167 168 169 170 171
            if recv['receipt']:
                return self.on_receipt(recv)
            self.on_normal_message(recv)

        recv = message['carbon_received']
172 173 174 175 176
        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:
mathieui's avatar
mathieui committed
177 178
            fixes.has_identity(
                self.core.xmpp,
179
                recv['from'].bare,
mathieui's avatar
mathieui committed
180 181 182
                identity='conference',
                on_true=functools.partial(ignore_message, recv),
                on_false=functools.partial(receive_message, recv))
mathieui's avatar
mathieui committed
183
            return
184 185 186 187 188 189 190
        else:
            receive_message(recv)

    def on_carbon_sent(self, message):
        """
        Carbon <sent/> received
        """
mathieui's avatar
mathieui committed
191

192 193
        def groupchat_private_message(sent):
            self.on_groupchat_private_message(sent, sent=True)
mathieui's avatar
mathieui committed
194

195
        def send_message(sent):
196
            sent['from'] = self.core.xmpp.boundjid.full
197 198 199
            self.on_normal_message(sent)

        sent = message['carbon_sent']
200 201 202 203 204
        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:
mathieui's avatar
mathieui committed
205 206 207 208
            fixes.has_identity(
                self.core.xmpp,
                sent['to'].server,
                identity='conference',
209
                on_true=functools.partial(groupchat_private_message, sent),
mathieui's avatar
mathieui committed
210
                on_false=functools.partial(send_message, sent))
211 212 213 214 215 216 217 218 219 220
        else:
            send_message(sent)

    ### Invites ###

    def on_groupchat_invitation(self, message):
        """
        Mediated invitation received
        """
        jid = message['from']
221
        if jid.bare in self.core.pending_invites:
222 223
            return
        # there are 2 'x' tags in the messages, making message['x'] useless
mathieui's avatar
mathieui committed
224 225 226 227 228
        invite = StanzaBase(
            self.core.xmpp,
            xml=message.xml.find(
                '{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite'
            ))
229 230
        # TODO: find out why pylint thinks "inviter" is a list
        #pylint: disable=no-member
231 232 233 234 235 236 237 238
        inviter = invite['from']
        reason = invite['reason']
        password = invite['password']
        msg = "You are invited to the room %s by %s" % (jid.full, inviter.full)
        if reason:
            msg += "because: %s" % reason
        if password:
            msg += ". The password is \"%s\"." % password
239
        self.core.information(msg, 'Info')
240 241 242
        if 'invite' in config.get('beep_on').split():
            curses.beep()
        logger.log_roster_change(inviter.full, 'invited you to %s' % jid.full)
243
        self.core.pending_invites[jid.bare] = inviter.full
244 245 246 247 248 249 250 251 252 253

    def on_groupchat_decline(self, decline):
        "Mediated invitation declined; skip for now"
        pass

    def on_groupchat_direct_invitation(self, message):
        """
        Direct invitation received
        """
        room = safeJID(message['groupchat_invite']['jid'])
254
        if room.bare in self.core.pending_invites:
mathieui's avatar
mathieui committed
255
            return
256

257 258 259 260 261 262 263 264 265 266 267 268 269
        inviter = message['from']
        reason = message['groupchat_invite']['reason']
        password = message['groupchat_invite']['password']
        continue_ = message['groupchat_invite']['continue']
        msg = "You are invited to the room %s by %s" % (room, inviter.full)

        if password:
            msg += ' (password: "%s")' % password
        if continue_:
            msg += '\nto continue the discussion'
        if reason:
            msg += "\nreason: %s" % reason

270
        self.core.information(msg, 'Info')
271 272 273
        if 'invite' in config.get('beep_on').split():
            curses.beep()

274
        self.core.pending_invites[room.bare] = inviter.full
275 276 277 278 279 280 281 282 283
        logger.log_roster_change(inviter.full, 'invited you to %s' % room.bare)

    ### "classic" messages ###

    def on_message(self, message):
        """
        When receiving private message from a muc OR a normal message
        (from one of our contacts)
        """
mathieui's avatar
mathieui committed
284 285 286
        if message.xml.find(
                '{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite'
        ) is not None:
287 288 289 290
            return
        if message['type'] == 'groupchat':
            return
        # Differentiate both type of messages, and call the appropriate handler.
291 292 293
        if self.is_known_muc_pm(message, message['from']):
            self.on_groupchat_private_message(message, sent=False)
            return
294
        self.on_normal_message(message)
295

296 297 298 299 300 301 302 303
    def on_encrypted_message(self, message):
        """
        When receiving an encrypted message
        """
        if message["body"]:
            return # Already being handled by on_message.
        self.on_message(message)

304 305 306 307 308
    def on_error_message(self, message):
        """
        When receiving any message with type="error"
        """
        jid_from = message['from']
309
        for tab in self.core.get_tabs(tabs.MucTab):
310
            if tab.jid.bare == jid_from.bare:
311
                if jid_from.full == jid_from.bare:
312
                    self.core.room_error(message, jid_from.bare)
313
                else:
314
                    text = self.core.get_error_message(message)
315 316
                    p_tab = self.core.tabs.by_name_and_class(
                        jid_from.full, tabs.PrivateTab)
317 318 319 320
                    if p_tab:
                        p_tab.add_error(text)
                    else:
                        self.core.information(text, 'Error')
321 322 323
                return
        tab = self.core.get_conversation_by_jid(message['from'], create=False)
        error_msg = self.core.get_error_message(message, deprecated=True)
324
        if not tab:
325 326
            self.core.information(error_msg, 'Error')
            return
327
        error = '\x19%s}%s\x19o' % (dump_tuple(get_theme().COLOR_CHAR_NACK),
mathieui's avatar
mathieui committed
328
                                    error_msg)
329
        if not tab.nack_message('\n' + error, message['id'], message['to']):
330
            tab.add_message(InfoMessage(error), typ=0)
331
            self.core.refresh_window()
332 333 334 335 336 337 338 339 340

    def on_normal_message(self, message):
        """
        When receiving "normal" messages (not a private message from a
        muc participant)
        """
        if message['type'] == 'error':
            return
        elif message['type'] == 'headline' and message['body']:
mathieui's avatar
mathieui committed
341 342
            return self.core.information(
                '%s says: %s' % (message['from'], message['body']), 'Headline')
343

mathieui's avatar
mathieui committed
344 345
        use_xhtml = config.get_by_tabname('enable_xhtml_im',
                                          message['from'].bare)
346
        tmp_dir = get_image_cache()
mathieui's avatar
mathieui committed
347
        body = xhtml.get_body_from_message_stanza(
mathieui's avatar
mathieui committed
348
            message, use_xhtml=use_xhtml, extract_images_to=tmp_dir)
349
        if not body:
350 351 352
            if not self.core.xmpp.plugin['xep_0380'].has_eme(message):
                return
            self.core.xmpp.plugin['xep_0380'].replace_body_with_eme(message)
mathieui's avatar
mathieui committed
353
            body = message['body']
mathieui's avatar
mathieui committed
354

355 356
        remote_nick = ''
        # normal message, we are the recipient
357
        if message['to'].bare == self.core.xmpp.boundjid.bare:
358 359 360 361 362 363 364 365
            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'):
mathieui's avatar
mathieui committed
366 367
                if message.xml.find(
                        '{http://jabber.org/protocol/nick}nick') is not None:
368
                    remote_nick = message['nick']['nick']
369
            if not remote_nick:
370 371 372 373 374
                remote_nick = conv_jid.user
                if not remote_nick:
                    remote_nick = conv_jid.full
            own = False
        # we wrote the message (happens with carbons)
375
        elif message['from'].bare == self.core.xmpp.boundjid.bare:
376
            conv_jid = message['to']
377
            jid = self.core.xmpp.boundjid
378
            color = get_theme().COLOR_OWN_NICK
379
            remote_nick = self.core.own_nick
380 381
            own = True
        # we are not part of that message, drop it
mathieui's avatar
mathieui committed
382
        else:
383 384
            return

385
        conversation = self.core.get_conversation_by_jid(conv_jid, create=True)
mathieui's avatar
mathieui committed
386 387
        if isinstance(conversation,
                      tabs.DynamicConversationTab) and conv_jid.resource:
388 389 390 391
            conversation.lock(conv_jid.resource)

        if not own and not conversation.nick:
            conversation.nick = remote_nick
392 393
        elif not own:
            remote_nick = conversation.get_nick()
394

395 396 397
        if not own:
            conversation.last_remote_message = datetime.now()

398
        self.core.events.trigger('conversation_msg', message, conversation)
399 400
        if not message['body']:
            return
mathieui's avatar
mathieui committed
401
        body = xhtml.get_body_from_message_stanza(
mathieui's avatar
mathieui committed
402
            message, use_xhtml=use_xhtml, extract_images_to=tmp_dir)
403 404 405
        delayed, date = common.find_delayed_tag(message)

        def try_modify():
Link Mauve's avatar
Link Mauve committed
406
            if message.xml.find('{urn:xmpp:message-correct:0}replace') is None:
407
                return False
408 409 410 411
            replaced_id = message['replace']['id']
            if replaced_id and config.get_by_tabname('group_corrections',
                                                     conv_jid.bare):
                try:
mathieui's avatar
mathieui committed
412 413 414 415 416 417
                    conversation.modify_message(
                        body,
                        replaced_id,
                        message['id'],
                        jid=jid,
                        nickname=remote_nick)
418 419 420 421 422 423
                    return True
                except CorrectionError:
                    log.debug('Unable to correct a message', exc_info=True)
            return False

        if not try_modify():
mathieui's avatar
mathieui committed
424
            conversation.add_message(
425 426 427 428 429 430 431 432 433 434 435
                PMessage(
                    txt=body,
                    time=date,
                    nickname=remote_nick,
                    nick_color=color,
                    history=delayed,
                    identifier=message['id'],
                    jid=jid,
                ),
                typ=1,
            )
436 437 438 439

        if not own and 'private' in config.get('beep_on').split():
            if not config.get_by_tabname('disable_beep', conv_jid.bare):
                curses.beep()
440
        if self.core.tabs.current_tab is not conversation:
441 442
            if not own:
                conversation.state = 'private'
443
                self.core.refresh_tab_win()
444 445
            else:
                conversation.set_state('normal')
446
                self.core.refresh_tab_win()
447
        else:
448
            self.core.refresh_window()
mathieui's avatar
mathieui committed
449

450
    async def on_0084_avatar(self, msg):
451 452 453 454 455 456
        jid = msg['from'].bare
        contact = roster[jid]
        if not contact:
            return
        log.debug('Received 0084 avatar update from %s', jid)
        try:
mathieui's avatar
mathieui committed
457 458
            metadata = msg['pubsub_event']['items']['item']['avatar_metadata'][
                'items']
459
        except Exception:
460
            log.debug('Failed getting metadata from 0084:', exc_info=True)
461 462
            return
        for info in metadata:
Link Mauve's avatar
Link Mauve committed
463 464 465
            avatar_hash = info['id']

            # First check whether we have it in cache.
mathieui's avatar
mathieui committed
466 467
            cached_avatar = self.core.avatar_cache.retrieve_by_jid(
                jid, avatar_hash)
468 469 470
            if cached_avatar:
                contact.avatar = cached_avatar
                log.debug('Using cached avatar for %s', jid)
Link Mauve's avatar
Link Mauve committed
471 472 473
                return

            # If we didn’t have any, query the data instead.
474 475
            if not info['url']:
                try:
mathieui's avatar
mathieui committed
476 477
                    result = await self.core.xmpp['xep_0084'].retrieve_avatar(
                        jid, avatar_hash, timeout=60)
mathieui's avatar
mathieui committed
478 479
                    avatar = result['pubsub']['items']['item']['avatar_data'][
                        'value']
480 481 482
                    if sha1(avatar).hexdigest().lower() != avatar_hash.lower():
                        raise Exception('Avatar sha1 doesn’t match 0084 hash.')
                    contact.avatar = avatar
483
                except Exception:
mathieui's avatar
mathieui committed
484 485 486 487
                    log.debug(
                        'Failed retrieving 0084 data from %s:',
                        jid,
                        exc_info=True)
Link Mauve's avatar
Link Mauve committed
488
                    continue
489 490
                log.debug('Received %s avatar: %s', jid, info['type'])

Link Mauve's avatar
Link Mauve committed
491
                # Now we save the data on the file system to not have to request it again.
mathieui's avatar
mathieui committed
492 493
                if not self.core.avatar_cache.store_by_jid(
                        jid, avatar_hash, contact.avatar):
mathieui's avatar
mathieui committed
494
                    log.debug(
495
                        'Failed writing %s’s avatar to cache:',
mathieui's avatar
mathieui committed
496 497
                        jid,
                        exc_info=True)
Link Mauve's avatar
Link Mauve committed
498 499
                return

500
    async def on_vcard_avatar(self, pres):
501
        jid = pres['from'].bare
502 503 504
        contact = roster[jid]
        if not contact:
            return
Link Mauve's avatar
Link Mauve committed
505 506 507 508
        avatar_hash = pres['vcard_temp_update']['photo']
        log.debug('Received vCard avatar update from %s: %s', jid, avatar_hash)

        # First check whether we have it in cache.
mathieui's avatar
mathieui committed
509 510
        cached_avatar = self.core.avatar_cache.retrieve_by_jid(
            jid, avatar_hash)
511 512 513
        if cached_avatar:
            contact.avatar = cached_avatar
            log.debug('Using cached avatar for %s', jid)
Link Mauve's avatar
Link Mauve committed
514 515 516
            return

        # If we didn’t have any, query the vCard instead.
517
        try:
518
            result = await self.core.xmpp['xep_0054'].get_vcard(
mathieui's avatar
mathieui committed
519
                jid, cached=True, timeout=60)
520
            avatar = result['vcard_temp']['PHOTO']
521 522 523 524
            binval = avatar['BINVAL']
            if sha1(binval).hexdigest().lower() != avatar_hash.lower():
                raise Exception('Avatar sha1 doesn’t match 0153 hash.')
            contact.avatar = binval
525
        except Exception:
526
            log.debug('Failed retrieving vCard from %s:', jid, exc_info=True)
527 528 529
            return
        log.debug('Received %s avatar: %s', jid, avatar['TYPE'])

Link Mauve's avatar
Link Mauve committed
530
        # Now we save the data on the file system to not have to request it again.
mathieui's avatar
mathieui committed
531 532 533 534
        if not self.core.avatar_cache.store_by_jid(jid, avatar_hash,
                                                   contact.avatar):
            log.debug(
                'Failed writing %s’s avatar to cache:', jid, exc_info=True)
Link Mauve's avatar
Link Mauve committed
535

536 537
    def on_nick_received(self, message):
        """
Maxime Buquet's avatar
Maxime Buquet committed
538
        Called when a pep notification for a user nickname
539 540 541 542 543 544
        is received
        """
        contact = roster[message['from'].bare]
        if not contact:
            return
        item = message['pubsub_event']['items']['item']
545
        if item.xml.find('{http://jabber.org/protocol/nick}nick') is not None:
546
            contact.name = item['nick']['nick']
mathieui's avatar
mathieui committed
547
        else:
548 549 550 551 552 553 554 555 556 557 558 559
            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
560 561 562
        xml_node = item.xml.find('{urn:xmpp:gaming:0}game')
        # list(xml_node) checks whether there are children or not.
        if xml_node is not None and list(xml_node):
563 564 565
            item = item['gaming']
            # only name and server_address are used for now
            contact.gaming = {
mathieui's avatar
mathieui committed
566 567 568 569 570 571 572 573
                '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'],
            }
mathieui's avatar
mathieui committed
574
        else:
575
            contact.gaming = {}
mathieui's avatar
mathieui committed
576

577
        if contact.gaming:
mathieui's avatar
mathieui committed
578 579 580
            logger.log_roster_change(
                contact.bare_jid, 'is playing %s' %
                (common.format_gaming_string(contact.gaming)))
mathieui's avatar
mathieui committed
581

mathieui's avatar
mathieui committed
582 583
        if old_gaming != contact.gaming and config.get_by_tabname(
                'display_gaming_notifications', contact.bare_jid):
584
            if contact.gaming:
mathieui's avatar
mathieui committed
585
                self.core.information(
mathieui's avatar
mathieui committed
586 587 588
                    '%s is playing %s' % (contact.bare_jid,
                                          common.format_gaming_string(
                                              contact.gaming)), 'Gaming')
589
            else:
mathieui's avatar
mathieui committed
590 591
                self.core.information(contact.bare_jid + ' stopped playing.',
                                      'Gaming')
592 593 594

    def on_mood_event(self, message):
        """
Maxime Buquet's avatar
Maxime Buquet committed
595
        Called when a pep notification for a user mood
596 597 598 599 600 601 602 603
        is received.
        """
        contact = roster[message['from'].bare]
        if not contact:
            return
        roster.modified()
        item = message['pubsub_event']['items']['item']
        old_mood = contact.mood
604 605 606
        xml_node = item.xml.find('{http://jabber.org/protocol/mood}mood')
        # list(xml_node) checks whether there are children or not.
        if xml_node is not None and list(xml_node):
607 608 609 610 611 612 613 614 615
            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 = ''
mathieui's avatar
mathieui committed
616
        else:
617 618 619
            contact.mood = ''

        if contact.mood:
mathieui's avatar
mathieui committed
620 621
            logger.log_roster_change(contact.bare_jid,
                                     'has now the mood: %s' % contact.mood)
622

mathieui's avatar
mathieui committed
623 624
        if old_mood != contact.mood and config.get_by_tabname(
                'display_mood_notifications', contact.bare_jid):
625
            if contact.mood:
mathieui's avatar
mathieui committed
626 627 628
                self.core.information(
                    'Mood from ' + contact.bare_jid + ': ' + contact.mood,
                    'Mood')
629
            else:
mathieui's avatar
mathieui committed
630
                self.core.information(
Kim Alvefur's avatar
Kim Alvefur committed
631
                    contact.bare_jid + ' stopped having their mood.', 'Mood')
632 633 634

    def on_activity_event(self, message):
        """
Maxime Buquet's avatar
Maxime Buquet committed
635
        Called when a pep notification for a user activity
636 637 638 639
        is received.
        """
        contact = roster[message['from'].bare]
        if not contact:
mathieui's avatar
mathieui committed
640
            return
641 642 643
        roster.modified()
        item = message['pubsub_event']['items']['item']
        old_activity = contact.activity
644 645 646
        xml_node = item.xml.find('{http://jabber.org/protocol/activity}activity')
        # list(xml_node) checks whether there are children or not.
        if xml_node is not None and list(xml_node):
647 648 649 650 651 652 653 654 655 656 657 658 659 660 661
            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 = ''
mathieui's avatar
mathieui committed
662 663 664 665
        else:
            contact.activity = ''

        if contact.activity:
mathieui's avatar
mathieui committed
666 667
            logger.log_roster_change(
                contact.bare_jid, 'has now the activity %s' % contact.activity)
668

mathieui's avatar
mathieui committed
669 670
        if old_activity != contact.activity and config.get_by_tabname(
                'display_activity_notifications', contact.bare_jid):
671
            if contact.activity:
mathieui's avatar
mathieui committed
672 673 674
                self.core.information(
                    'Activity from ' + contact.bare_jid + ': ' +
                    contact.activity, 'Activity')
675
            else:
mathieui's avatar
mathieui committed
676
                self.core.information(
Kim Alvefur's avatar
Kim Alvefur committed
677
                    contact.bare_jid + ' stopped doing their activity.',
mathieui's avatar
mathieui committed
678
                    'Activity')
679 680 681

    def on_tune_event(self, message):
        """
Maxime Buquet's avatar
Maxime Buquet committed
682
        Called when a pep notification for a user tune
683 684 685 686 687 688 689 690
        is received
        """
        contact = roster[message['from'].bare]
        if not contact:
            return
        roster.modified()
        item = message['pubsub_event']['items']['item']
        old_tune = contact.tune
691 692 693
        xml_node = item.xml.find('{http://jabber.org/protocol/tune}tune')
        # list(xml_node) checks whether there are children or not.
        if xml_node is not None and list(xml_node):
694 695
            item = item['tune']
            contact.tune = {
mathieui's avatar
mathieui committed
696 697 698 699 700 701 702 703
                'artist': item['artist'],
                'length': item['length'],
                'rating': item['rating'],
                'source': item['source'],
                'title': item['title'],
                'track': item['track'],
                'uri': item['uri']
            }
mathieui's avatar
mathieui committed
704
        else:
705 706
            contact.tune = {}

mathieui's avatar
mathieui committed
707
        if contact.tune:
mathieui's avatar
mathieui committed
708 709 710
            logger.log_roster_change(
                message['from'].bare, 'is now listening to %s' %
                common.format_tune_string(contact.tune))
mathieui's avatar
mathieui committed
711

mathieui's avatar
mathieui committed
712 713
        if old_tune != contact.tune and config.get_by_tabname(
                'display_tune_notifications', contact.bare_jid):
714
            if contact.tune:
715
                self.core.information(
mathieui's avatar
mathieui committed
716 717
                    'Tune from ' + message['from'].bare + ': ' +
                    common.format_tune_string(contact.tune), 'Tune')
718
            else:
mathieui's avatar
mathieui committed
719 720
                self.core.information(
                    contact.bare_jid + ' stopped listening to music.', 'Tune')
mathieui's avatar
mathieui committed
721

722 723 724 725 726
    def on_groupchat_message(self, message):
        """
        Triggered whenever a message is received from a multi-user chat room.
        """
        room_from = message['from'].bare
mathieui's avatar
mathieui committed
727

mathieui's avatar
mathieui committed
728
        if message['type'] == 'error':  # Check if it's an error
729 730
            self.core.room_error(message, room_from)
            return
mathieui's avatar
mathieui committed
731

732
        tab = self.core.tabs.by_name_and_class(room_from, tabs.MucTab)
733
        if not tab:
mathieui's avatar
mathieui committed
734 735 736 737
            self.core.information(
                "message received for a non-existing room: %s" % (room_from))
            muc.leave_groupchat(
                self.core.xmpp, room_from, self.core.own_nick, msg='')
738
            return
mathieui's avatar
mathieui committed
739

740 741 742 743
        nick_from = message['mucnick']
        user = tab.get_user_by_name(nick_from)
        if user and user in tab.ignores:
            return
mathieui's avatar
mathieui committed
744

745
        self.core.events.trigger('muc_msg', message, tab)
mathieui's avatar
mathieui committed
746
        use_xhtml = config.get_by_tabname('enable_xhtml_im', room_from)
747
        tmp_dir = get_image_cache()
mathieui's avatar
mathieui committed
748
        body = xhtml.get_body_from_message_stanza(
mathieui's avatar
mathieui committed
749
            message, use_xhtml=use_xhtml, extract_images_to=tmp_dir)
Maxime Buquet's avatar
Maxime Buquet committed
750 751 752 753 754 755 756 757 758

        # 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.

759 760
        if not body:
            return
mathieui's avatar
mathieui committed
761

762 763
        old_state = tab.state
        delayed, date = common.find_delayed_tag(message)
764 765 766 767 768

        history = (tab.last_message_was_history is None and delayed) or \
            (tab.last_message_was_history and delayed)
        tab.last_message_was_history = history

769
        replaced = False
mathieui's avatar
mathieui committed
770
        if message.xml.find('{urn:xmpp:message-correct:0}replace') is not None:
771
            replaced_id = message['replace']['id']
772
            if replaced_id != '' and config.get_by_tabname(
mathieui's avatar
mathieui committed
773
                    'group_corrections', message['from'].bare):
774 775
                try:
                    delayed_date = date or datetime.now()
mathieui's avatar
mathieui committed
776 777 778 779
                    if tab.modify_message(
                            body,
                            replaced_id,
                            message['id'],
780
                            time=delayed_date,
781
                            delayed=delayed,
mathieui's avatar
mathieui committed
782 783
                            nickname=nick_from,
                            user=user):
784 785 786 787
                        self.core.events.trigger('highlight', message, tab)
                    replaced = True
                except CorrectionError:
                    log.debug('Unable to correct a message', exc_info=True)
788 789 790 791

        if not replaced:
            # Messages coming from MUC barejid (Server maintenance, IRC mode
            # changes from biboumi, etc.) are displayed as info messages.
792
            highlight = False
793
            if message['from'].resource:
mathieui's avatar
mathieui committed
794
                highlight = tab.message_is_highlight(body, nick_from, delayed)
795
                ui_msg = PMessage(
796 797 798
                    txt=body,
                    time=date,
                    nickname=nick_from,
799
                    history=history,
800
                    delayed=delayed,
801 802 803
                    identifier=message['id'],
                    jid=message['from'],
                    user=user,
mathieui's avatar
mathieui committed
804
                    highlight=highlight,
805 806 807 808 809 810 811 812 813
                )
                typ = 1
            else:
                ui_msg = InfoMessage(
                    txt=body,
                    time=date,
                    identifier=message['id'],
                )
                typ = 2
mathieui's avatar
mathieui committed
814 815
            tab.add_message(ui_msg, typ)
            if highlight:
816
                self.core.events.trigger('highlight', message, tab)
mathieui's avatar
mathieui committed
817

818
        if message['from'].resource == tab.own_nick:
Maxime Buquet's avatar
Maxime Buquet committed
819
            tab.set_last_sent_message(message, correct=replaced)
820

821
        if tab is self.core.tabs.current_tab:
822
            tab.text_win.refresh()
823
            tab.info_header.refresh(tab, tab.text_win, user=tab.own_user)
824
            tab.input.refresh()
825
            self.core.doupdate()
826
        elif tab.state != old_state:
827
            self.core.refresh_tab_win()
828
            current = self.core.tabs.current_tab
829 830
            if hasattr(current, 'input') and current.input:
                current.input.refresh()
831
            self.core.doupdate()
832 833 834

        if 'message' in config.get('beep_on').split():
            if (not config.get_by_tabname('disable_beep', room_from)
835
                    and self.core.own_nick != message['from'].resource):
836 837 838 839
                curses.beep()

    def on_muc_own_nickchange(self, muc):
        "We changed our nick in a MUC"
840
        for tab in self.core.get_tabs(tabs.PrivateTab):
841 842 843
            if tab.parent_muc == muc:
                tab.own_nick = muc.own_nick

844
    def on_groupchat_private_message(self, message, sent):
845 846 847
        """
        We received a Private Message (from someone in a Muc)
        """
848 849 850
        jid = message['to'] if sent else message['from']
        with_nick = jid.resource
        if not with_nick:
851 852
            self.on_groupchat_message(message)
            return
853 854

        room_from = jid.bare
mathieui's avatar
mathieui committed
855
        use_xhtml = config.get_by_tabname('enable_xhtml_im', jid.bare)
856
        tmp_dir = get_image_cache()
mathieui's avatar
mathieui committed
857
        body = xhtml.get_body_from_message_stanza(
mathieui's avatar
mathieui committed
858
            message, use_xhtml=use_xhtml, extract_images_to=tmp_dir)
859
        tab = self.core.tabs.by_name_and_class(
mathieui's avatar
mathieui committed
860 861
            jid.full,
            tabs.PrivateTab)  # get the tab with the private conversation
862
        ignore = config.get_by_tabname('ignore_private', room_from)
mathieui's avatar
mathieui committed
863
        if not tab:  # It's the first message we receive: create the tab
864
            if body and not ignore:
865
                tab = self.core.open_private_window(room_from, with_nick,
mathieui's avatar
mathieui committed
866
                                                    False)
867 868 869
        # Tab can still be None here, when receiving carbons of a MUC-PM for
        # example
        sender_nick = (tab and tab.own_nick
mathieui's avatar
mathieui committed
870
                       or self.core.own_nick) if sent else with_nick
871
        if ignore and not sent:
872
            self.core.events.trigger('ignored_private', message, tab)
873 874
            msg = config.get_by_tabname('private_auto_response', room_from)
            if msg and body:
mathieui's avatar
mathieui committed
875 876
                self.core.xmpp.send_message(
                    mto=jid.full, mbody=msg, mtype='chat')
877
            return
878
        self.core.events.trigger('private_msg', message, tab)
mathieui's avatar
mathieui committed
879
        body = xhtml.get_body_from_message_stanza(
mathieui's avatar
mathieui committed
880
            message, use_xhtml=use_xhtml, extract_images_to=tmp_dir)
881 882 883
        if not body or not tab:
            return
        replaced = False
884
        user = tab.parent_muc.get_user_by_name(with_nick)
mathieui's avatar
mathieui committed
885
        if message.xml.find('{urn:xmpp:message-correct:0}replace') is not None:
886
            replaced_id = message['replace']['id']
887
            if replaced_id != '' and config.get_by_tabname(
mathieui's avatar
mathieui committed
888
                    'group_corrections', room_from):
889
                try:
mathieui's avatar
mathieui committed
890 891 892 893 894 895
                    tab.modify_message(
                        body,
                        replaced_id,
                        message['id'],
                        user=user,
                        jid=message['from'],
896
                        nickname=sender_nick)
897 898 899
                    replaced = True
                except CorrectionError:
                    log.debug('Unable to correct a message', exc_info=True)
900
        if not replaced:
mathieui's avatar
mathieui committed
901
            tab.add_message(
902 903 904 905 906 907 908 909 910 911
                PMessage(
                    txt=body,
                    nickname=sender_nick,
                    nick_color=get_theme().COLOR_OWN_NICK if sent else None,
                    user=user,
                    identifier=message['id'],
                    jid=message['from'],
                ),
                typ=1,
            )
912
        if sent:
Maxime Buquet's avatar
Maxime Buquet committed
913
            tab.set_last_sent_message(message, correct=replaced)
914 915
        else:
            tab.last_remote_message = datetime.now()
916

917
        if not sent and 'private' in config.get('beep_on').split():
918 919
            if not config.get_by_tabname('disable_beep', jid.full):
                curses.beep()
920
        if tab is self.core.tabs.current_tab:
921
            self.core.refresh_window()
922
        else:
923
            tab.state = 'normal' if sent else 'private'
924
            self.core.refresh_tab_win()
mathieui's avatar
mathieui committed
925

926
    ### Chatstates ###
mathieui's avatar
mathieui committed
927

928
    def on_chatstate_active(self, message):
929
        self._on_chatstate(message, "active")
mathieui's avatar
mathieui committed
930

931
    def on_chatstate_inactive(self, message):
932
        self._on_chatstate(message, "inactive")
mathieui's avatar
mathieui committed
933

934
    def on_chatstate_composing(self, message):
935
        self._on_chatstate(message, "composing")
mathieui's avatar
mathieui committed
936

937
    def on_chatstate_paused(self, message):
938
        self._on_chatstate(message, "paused")
mathieui's avatar
mathieui committed
939

940
    def on_chatstate_gone(self, message):
941
        self._on_chatstate(message, "gone")
mathieui's avatar
mathieui committed
942

943
    def _on_chatstate(self, message, state):
944
        if message['type'] == 'chat':
945
            if not self._on_chatstate_normal_conversation(message, state):
946 947
                tab = self.core.tabs.by_name_and_class(message['from'].full,
                                                       tabs.PrivateTab)
948 949
                if not tab:
                    return
950
                self._on_chatstate_private_conversation(message, state)
951 952 953
        elif message['type'] == 'groupchat':
            self.on_chatstate_groupchat_conversation(message, state)

954 955
    def _on_chatstate_normal_conversation(self, message, state):
        tab = self.core.get_conversation_by_jid(message['from'], False)
956 957
        if not tab:
            return False
958
        self.core.events.trigger('normal_chatstate', message, tab)
959 960
        tab.chatstate = state
        if state == 'gone' and isinstance(tab, tabs.DynamicConversationTab):
mathieui's avatar
mathieui committed
961
            tab.unlock()
962
        if tab == self.core.tabs.current_tab:
963
            tab.refresh_info_header()
964
            self.core.doupdate()
965 966
        else:
            _composing_tab_state(tab, state)
967
            self.core.refresh_tab_win()
968
        return True
mathieui's avatar
mathieui committed
969

970
    def _on_chatstate_private_conversation(self, message, state):
971 972 973
        """
        Chatstate received in a private conversation from a MUC
        """
974 975
        tab = self.core.tabs.by_name_and_class(message['from'].full,
                                               tabs.PrivateTab)
976 977
        if not tab:
            return
978
        self.core.events.trigger('private_chatstate', message, tab)
979
        tab.chatstate = state
980
        if tab == self.core.tabs.current_tab:
981
            tab.refresh_info_header()
982
            self.core.doupdate()
mathieui's avatar
mathieui committed
983
        else:
984
            _composing_tab_state(tab, state)
985
            self.core.refresh_tab_win()
986 987 988 989 990 991 992

    def on_chatstate_groupchat_conversation(self, message, state):
        """
        Chatstate received in a MUC
        """
        nick = message['mucnick']
        room_from = message.get_mucroom()
993
        tab = self.core.tabs.by_name_and_class(room_from, tabs.MucTab)
994
        if tab and tab.get_user_by_name(nick):
995
            self.core.events.trigger('muc_chatstate', message, tab)
996
            tab.get_user_by_name(nick).chatstate = state
997
        if tab == self.core.tabs.current_tab:
998
            if not self.core.size.tab_degrade_x:
999 1000
                tab.user_win.refresh(tab.users)
            tab.input.refresh()
1001
            self.core.doupdate()
1002 1003
        else:
            _composing_tab_state(tab, state)
1004
            self.core.refresh_tab_win()
mathieui's avatar
mathieui committed
1005

1006 1007 1008 1009 1010 1011 1012
    @staticmethod
    def _format_error(error):
            error_condition = error['condition']
            error_text = err