handlers.py 63 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__)

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

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

26 27 28 29 30 31 32
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
33
from poezio.config import config, get_image_cache
34
from poezio.core.structs import Status
35 36 37 38 39
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
mathieui's avatar
mathieui committed
40

mathieui's avatar
mathieui committed
41
from poezio.core.commands import dumb_callback
mathieui's avatar
mathieui committed
42

43 44 45 46 47 48
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)
49
    PYGMENTS = True
50
except ImportError:
51
    PYGMENTS = False
52

53 54 55 56 57 58 59
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.

60
SHA-256 of the old certificate (SPKI): %s
61

62
SHA-256 of the new certificate (SPKI): %s
63 64 65 66 67 68 69 70 71 72 73
"""

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.

"""

74 75 76 77 78 79 80 81 82

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
83

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

mathieui's avatar
mathieui committed
97 98
        self.core.xmpp.plugin['xep_0030'].get_info(
            jid=self.core.xmpp.boundjid.domain, callback=callback)
99

100 101 102 103 104
    def find_identities(self, _):
        asyncio.ensure_future(
            self.core.xmpp['xep_0030'].get_info_from_domain(),
        )

105 106 107 108
    def on_carbon_received(self, message):
        """
        Carbon <received/> received
        """
mathieui's avatar
mathieui committed
109

110 111 112
        def ignore_message(recv):
            log.debug('%s has category conference, ignoring carbon',
                      recv['from'].server)
mathieui's avatar
mathieui committed
113

114
        def receive_message(recv):
115
            recv['to'] = self.core.xmpp.boundjid.full
116 117 118 119 120
            if recv['receipt']:
                return self.on_receipt(recv)
            self.on_normal_message(recv)

        recv = message['carbon_received']
mathieui's avatar
mathieui committed
121 122 123 124 125 126 127 128
        if (recv['from'].bare not in roster
                or roster[recv['from'].bare].subscription == 'none'):
            fixes.has_identity(
                self.core.xmpp,
                recv['from'].server,
                identity='conference',
                on_true=functools.partial(ignore_message, recv),
                on_false=functools.partial(receive_message, recv))
mathieui's avatar
mathieui committed
129
            return
130 131 132 133 134 135 136
        else:
            receive_message(recv)

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

138 139
        def groupchat_private_message(sent):
            self.on_groupchat_private_message(sent, sent=True)
mathieui's avatar
mathieui committed
140

141
        def send_message(sent):
142
            sent['from'] = self.core.xmpp.boundjid.full
143 144 145
            self.on_normal_message(sent)

        sent = message['carbon_sent']
ge0rg's avatar
ge0rg committed
146
        # todo: implement proper MUC detection logic
mathieui's avatar
mathieui committed
147 148 149
        if (sent['to'].resource
                and (sent['to'].bare not in roster
                     or roster[sent['to'].bare].subscription == 'none')):
mathieui's avatar
mathieui committed
150 151 152 153
            fixes.has_identity(
                self.core.xmpp,
                sent['to'].server,
                identity='conference',
154
                on_true=functools.partial(groupchat_private_message, sent),
mathieui's avatar
mathieui committed
155
                on_false=functools.partial(send_message, sent))
156 157 158 159 160 161 162 163 164 165
        else:
            send_message(sent)

    ### Invites ###

    def on_groupchat_invitation(self, message):
        """
        Mediated invitation received
        """
        jid = message['from']
166
        if jid.bare in self.core.pending_invites:
167 168
            return
        # there are 2 'x' tags in the messages, making message['x'] useless
mathieui's avatar
mathieui committed
169 170 171 172 173
        invite = StanzaBase(
            self.core.xmpp,
            xml=message.xml.find(
                '{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite'
            ))
174 175
        # TODO: find out why pylint thinks "inviter" is a list
        #pylint: disable=no-member
176 177 178 179 180 181 182 183
        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
184
        self.core.information(msg, 'Info')
185 186 187
        if 'invite' in config.get('beep_on').split():
            curses.beep()
        logger.log_roster_change(inviter.full, 'invited you to %s' % jid.full)
188
        self.core.pending_invites[jid.bare] = inviter.full
189 190 191 192 193 194 195 196 197 198

    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'])
199
        if room.bare in self.core.pending_invites:
mathieui's avatar
mathieui committed
200
            return
201

202 203 204 205 206 207 208 209 210 211 212 213 214
        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

215
        self.core.information(msg, 'Info')
216 217 218
        if 'invite' in config.get('beep_on').split():
            curses.beep()

219
        self.core.pending_invites[room.bare] = inviter.full
220 221 222 223 224 225 226 227 228
        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
229 230 231
        if message.xml.find(
                '{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite'
        ) is not None:
232 233 234 235 236
            return
        if message['type'] == 'groupchat':
            return
        # Differentiate both type of messages, and call the appropriate handler.
        jid_from = message['from']
237
        for tab in self.core.get_tabs(tabs.MucTab):
238
            if tab.name == jid_from.bare:
239
                if jid_from.resource:
240
                    self.on_groupchat_private_message(message, sent=False)
241 242
                    return
        self.on_normal_message(message)
243 244 245 246 247 248

    def on_error_message(self, message):
        """
        When receiving any message with type="error"
        """
        jid_from = message['from']
249
        for tab in self.core.get_tabs(tabs.MucTab):
250
            if tab.name == jid_from.bare:
251
                if jid_from.full == jid_from.bare:
252
                    self.core.room_error(message, jid_from.bare)
253
                else:
254
                    text = self.core.get_error_message(message)
255 256
                    p_tab = self.core.tabs.by_name_and_class(
                        jid_from.full, tabs.PrivateTab)
257 258 259 260
                    if p_tab:
                        p_tab.add_error(text)
                    else:
                        self.core.information(text, 'Error')
261 262 263
                return
        tab = self.core.get_conversation_by_jid(message['from'], create=False)
        error_msg = self.core.get_error_message(message, deprecated=True)
264
        if not tab:
265 266
            self.core.information(error_msg, 'Error')
            return
267
        error = '\x19%s}%s\x19o' % (dump_tuple(get_theme().COLOR_CHAR_NACK),
mathieui's avatar
mathieui committed
268
                                    error_msg)
269 270
        if not tab.nack_message('\n' + error, message['id'], message['to']):
            tab.add_message(error, typ=0)
271
            self.core.refresh_window()
272 273 274 275 276 277 278 279 280

    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
281 282
            return self.core.information(
                '%s says: %s' % (message['from'], message['body']), 'Headline')
283

mathieui's avatar
mathieui committed
284 285
        use_xhtml = config.get_by_tabname('enable_xhtml_im',
                                          message['from'].bare)
286
        tmp_dir = get_image_cache()
mathieui's avatar
mathieui committed
287
        body = xhtml.get_body_from_message_stanza(
mathieui's avatar
mathieui committed
288
            message, use_xhtml=use_xhtml, extract_images_to=tmp_dir)
289
        if not body:
290 291 292
            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
293
            body = message['body']
mathieui's avatar
mathieui committed
294

295 296
        remote_nick = ''
        # normal message, we are the recipient
297
        if message['to'].bare == self.core.xmpp.boundjid.bare:
298 299 300 301 302 303 304 305
            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
306 307
                if message.xml.find(
                        '{http://jabber.org/protocol/nick}nick') is not None:
308
                    remote_nick = message['nick']['nick']
309
            if not remote_nick:
310 311 312 313 314
                remote_nick = conv_jid.user
                if not remote_nick:
                    remote_nick = conv_jid.full
            own = False
        # we wrote the message (happens with carbons)
315
        elif message['from'].bare == self.core.xmpp.boundjid.bare:
316
            conv_jid = message['to']
317
            jid = self.core.xmpp.boundjid
318
            color = get_theme().COLOR_OWN_NICK
319
            remote_nick = self.core.own_nick
320 321
            own = True
        # we are not part of that message, drop it
mathieui's avatar
mathieui committed
322
        else:
323 324
            return

325
        conversation = self.core.get_conversation_by_jid(conv_jid, create=True)
mathieui's avatar
mathieui committed
326 327
        if isinstance(conversation,
                      tabs.DynamicConversationTab) and conv_jid.resource:
328 329 330 331
            conversation.lock(conv_jid.resource)

        if not own and not conversation.nick:
            conversation.nick = remote_nick
332 333
        elif not own:
            remote_nick = conversation.get_nick()
334

335 336 337
        if not own:
            conversation.last_remote_message = datetime.now()

338
        self.core.events.trigger('conversation_msg', message, conversation)
339 340
        if not message['body']:
            return
mathieui's avatar
mathieui committed
341
        body = xhtml.get_body_from_message_stanza(
mathieui's avatar
mathieui committed
342
            message, use_xhtml=use_xhtml, extract_images_to=tmp_dir)
343 344 345
        delayed, date = common.find_delayed_tag(message)

        def try_modify():
Link Mauve's avatar
Link Mauve committed
346
            if message.xml.find('{urn:xmpp:message-correct:0}replace') is None:
347
                return False
348 349 350 351
            replaced_id = message['replace']['id']
            if replaced_id and config.get_by_tabname('group_corrections',
                                                     conv_jid.bare):
                try:
mathieui's avatar
mathieui committed
352 353 354 355 356 357
                    conversation.modify_message(
                        body,
                        replaced_id,
                        message['id'],
                        jid=jid,
                        nickname=remote_nick)
358 359 360 361 362 363
                    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
364 365 366 367 368 369 370 371 372
            conversation.add_message(
                body,
                date,
                nickname=remote_nick,
                nick_color=color,
                history=delayed,
                identifier=message['id'],
                jid=jid,
                typ=1)
373 374 375 376

        if not own and 'private' in config.get('beep_on').split():
            if not config.get_by_tabname('disable_beep', conv_jid.bare):
                curses.beep()
377
        if self.core.tabs.current_tab is not conversation:
378 379
            if not own:
                conversation.state = 'private'
380
                self.core.refresh_tab_win()
381 382
            else:
                conversation.set_state('normal')
383
                self.core.refresh_tab_win()
384
        else:
385
            self.core.refresh_window()
mathieui's avatar
mathieui committed
386

387
    async def on_0084_avatar(self, msg):
388 389 390 391 392 393
        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
394 395
            metadata = msg['pubsub_event']['items']['item']['avatar_metadata'][
                'items']
396
        except Exception:
397
            log.debug('Failed getting metadata from 0084:', exc_info=True)
398 399
            return
        for info in metadata:
Link Mauve's avatar
Link Mauve committed
400 401 402
            avatar_hash = info['id']

            # First check whether we have it in cache.
mathieui's avatar
mathieui committed
403 404
            cached_avatar = self.core.avatar_cache.retrieve_by_jid(
                jid, avatar_hash)
405 406 407
            if cached_avatar:
                contact.avatar = cached_avatar
                log.debug('Using cached avatar for %s', jid)
Link Mauve's avatar
Link Mauve committed
408 409 410
                return

            # If we didn’t have any, query the data instead.
411 412
            if not info['url']:
                try:
mathieui's avatar
mathieui committed
413 414
                    result = await self.core.xmpp['xep_0084'].retrieve_avatar(
                        jid, avatar_hash, timeout=60)
mathieui's avatar
mathieui committed
415 416
                    avatar = result['pubsub']['items']['item']['avatar_data'][
                        'value']
417 418 419
                    if sha1(avatar).hexdigest().lower() != avatar_hash.lower():
                        raise Exception('Avatar sha1 doesn’t match 0084 hash.')
                    contact.avatar = avatar
420
                except Exception:
mathieui's avatar
mathieui committed
421 422 423 424
                    log.debug(
                        'Failed retrieving 0084 data from %s:',
                        jid,
                        exc_info=True)
Link Mauve's avatar
Link Mauve committed
425
                    continue
426 427
                log.debug('Received %s avatar: %s', jid, info['type'])

Link Mauve's avatar
Link Mauve committed
428
                # Now we save the data on the file system to not have to request it again.
mathieui's avatar
mathieui committed
429 430
                if not self.core.avatar_cache.store_by_jid(
                        jid, avatar_hash, contact.avatar):
mathieui's avatar
mathieui committed
431
                    log.debug(
432
                        'Failed writing %s’s avatar to cache:',
mathieui's avatar
mathieui committed
433 434
                        jid,
                        exc_info=True)
Link Mauve's avatar
Link Mauve committed
435 436
                return

437
    async def on_vcard_avatar(self, pres):
438
        jid = pres['from'].bare
439 440 441
        contact = roster[jid]
        if not contact:
            return
Link Mauve's avatar
Link Mauve committed
442 443 444 445
        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
446 447
        cached_avatar = self.core.avatar_cache.retrieve_by_jid(
            jid, avatar_hash)
448 449 450
        if cached_avatar:
            contact.avatar = cached_avatar
            log.debug('Using cached avatar for %s', jid)
Link Mauve's avatar
Link Mauve committed
451 452 453
            return

        # If we didn’t have any, query the vCard instead.
454
        try:
455
            result = await self.core.xmpp['xep_0054'].get_vcard(
mathieui's avatar
mathieui committed
456
                jid, cached=True, timeout=60)
457
            avatar = result['vcard_temp']['PHOTO']
458 459 460 461
            binval = avatar['BINVAL']
            if sha1(binval).hexdigest().lower() != avatar_hash.lower():
                raise Exception('Avatar sha1 doesn’t match 0153 hash.')
            contact.avatar = binval
462
        except Exception:
463
            log.debug('Failed retrieving vCard from %s:', jid, exc_info=True)
464 465 466
            return
        log.debug('Received %s avatar: %s', jid, avatar['TYPE'])

Link Mauve's avatar
Link Mauve committed
467
        # Now we save the data on the file system to not have to request it again.
mathieui's avatar
mathieui committed
468 469 470 471
        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
472

473 474 475 476 477 478 479 480 481
    def on_nick_received(self, message):
        """
        Called when a pep notification for an user nickname
        is received
        """
        contact = roster[message['from'].bare]
        if not contact:
            return
        item = message['pubsub_event']['items']['item']
482
        if item.xml.find('{http://jabber.org/protocol/nick}nick') is not None:
483
            contact.name = item['nick']['nick']
mathieui's avatar
mathieui committed
484
        else:
485 486 487 488 489 490 491 492 493 494 495 496
            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
497
        if item.xml.find('{urn:xmpp:gaming:0}gaming') is not None:
498 499 500
            item = item['gaming']
            # only name and server_address are used for now
            contact.gaming = {
mathieui's avatar
mathieui committed
501 502 503 504 505 506 507 508
                '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
509
        else:
510
            contact.gaming = {}
mathieui's avatar
mathieui committed
511

512
        if contact.gaming:
mathieui's avatar
mathieui committed
513 514 515
            logger.log_roster_change(
                contact.bare_jid, 'is playing %s' %
                (common.format_gaming_string(contact.gaming)))
mathieui's avatar
mathieui committed
516

mathieui's avatar
mathieui committed
517 518
        if old_gaming != contact.gaming and config.get_by_tabname(
                'display_gaming_notifications', contact.bare_jid):
519
            if contact.gaming:
mathieui's avatar
mathieui committed
520
                self.core.information(
mathieui's avatar
mathieui committed
521 522 523
                    '%s is playing %s' % (contact.bare_jid,
                                          common.format_gaming_string(
                                              contact.gaming)), 'Gaming')
524
            else:
mathieui's avatar
mathieui committed
525 526
                self.core.information(contact.bare_jid + ' stopped playing.',
                                      'Gaming')
527 528 529 530 531 532 533 534 535 536 537 538

    def on_mood_event(self, message):
        """
        Called when a pep notification for an 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
539
        if item.xml.find('{http://jabber.org/protocol/mood}mood') is not None:
540 541 542 543 544 545 546 547 548
            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
549
        else:
550 551 552
            contact.mood = ''

        if contact.mood:
mathieui's avatar
mathieui committed
553 554
            logger.log_roster_change(contact.bare_jid,
                                     'has now the mood: %s' % contact.mood)
555

mathieui's avatar
mathieui committed
556 557
        if old_mood != contact.mood and config.get_by_tabname(
                'display_mood_notifications', contact.bare_jid):
558
            if contact.mood:
mathieui's avatar
mathieui committed
559 560 561
                self.core.information(
                    'Mood from ' + contact.bare_jid + ': ' + contact.mood,
                    'Mood')
562
            else:
mathieui's avatar
mathieui committed
563
                self.core.information(
Kim Alvefur's avatar
Kim Alvefur committed
564
                    contact.bare_jid + ' stopped having their mood.', 'Mood')
565 566 567 568 569 570 571 572

    def on_activity_event(self, message):
        """
        Called when a pep notification for an user activity
        is received.
        """
        contact = roster[message['from'].bare]
        if not contact:
mathieui's avatar
mathieui committed
573
            return
574 575 576
        roster.modified()
        item = message['pubsub_event']['items']['item']
        old_activity = contact.activity
mathieui's avatar
mathieui committed
577 578
        if item.xml.find(
                '{http://jabber.org/protocol/activity}activity') is not None:
579 580 581 582 583 584 585 586 587 588 589 590 591 592 593
            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
594 595 596 597
        else:
            contact.activity = ''

        if contact.activity:
mathieui's avatar
mathieui committed
598 599
            logger.log_roster_change(
                contact.bare_jid, 'has now the activity %s' % contact.activity)
600

mathieui's avatar
mathieui committed
601 602
        if old_activity != contact.activity and config.get_by_tabname(
                'display_activity_notifications', contact.bare_jid):
603
            if contact.activity:
mathieui's avatar
mathieui committed
604 605 606
                self.core.information(
                    'Activity from ' + contact.bare_jid + ': ' +
                    contact.activity, 'Activity')
607
            else:
mathieui's avatar
mathieui committed
608
                self.core.information(
Kim Alvefur's avatar
Kim Alvefur committed
609
                    contact.bare_jid + ' stopped doing their activity.',
mathieui's avatar
mathieui committed
610
                    'Activity')
611 612 613 614 615 616 617 618 619 620 621 622

    def on_tune_event(self, message):
        """
        Called when a pep notification for an 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
623
        if item.xml.find('{http://jabber.org/protocol/tune}tune') is not None:
624 625
            item = item['tune']
            contact.tune = {
mathieui's avatar
mathieui committed
626 627 628 629 630 631 632 633
                '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
634
        else:
635 636
            contact.tune = {}

mathieui's avatar
mathieui committed
637
        if contact.tune:
mathieui's avatar
mathieui committed
638 639 640
            logger.log_roster_change(
                message['from'].bare, 'is now listening to %s' %
                common.format_tune_string(contact.tune))
mathieui's avatar
mathieui committed
641

mathieui's avatar
mathieui committed
642 643
        if old_tune != contact.tune and config.get_by_tabname(
                'display_tune_notifications', contact.bare_jid):
644
            if contact.tune:
645
                self.core.information(
mathieui's avatar
mathieui committed
646 647
                    'Tune from ' + message['from'].bare + ': ' +
                    common.format_tune_string(contact.tune), 'Tune')
648
            else:
mathieui's avatar
mathieui committed
649 650
                self.core.information(
                    contact.bare_jid + ' stopped listening to music.', 'Tune')
mathieui's avatar
mathieui committed
651

652 653 654 655 656 657 658
    def on_groupchat_message(self, message):
        """
        Triggered whenever a message is received from a multi-user chat room.
        """
        if message['subject']:
            return
        room_from = message['from'].bare
mathieui's avatar
mathieui committed
659

mathieui's avatar
mathieui committed
660
        if message['type'] == 'error':  # Check if it's an error
661 662
            self.core.room_error(message, room_from)
            return
mathieui's avatar
mathieui committed
663

664
        tab = self.core.tabs.by_name_and_class(room_from, tabs.MucTab)
665
        if not tab:
mathieui's avatar
mathieui committed
666 667 668 669
            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='')
670
            return
mathieui's avatar
mathieui committed
671

672 673 674 675
        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
676

677
        self.core.events.trigger('muc_msg', message, tab)
mathieui's avatar
mathieui committed
678
        use_xhtml = config.get_by_tabname('enable_xhtml_im', room_from)
679
        tmp_dir = get_image_cache()
mathieui's avatar
mathieui committed
680
        body = xhtml.get_body_from_message_stanza(
mathieui's avatar
mathieui committed
681
            message, use_xhtml=use_xhtml, extract_images_to=tmp_dir)
682 683
        if not body:
            return
mathieui's avatar
mathieui committed
684

685 686 687
        old_state = tab.state
        delayed, date = common.find_delayed_tag(message)
        replaced = False
mathieui's avatar
mathieui committed
688
        if message.xml.find('{urn:xmpp:message-correct:0}replace') is not None:
689
            replaced_id = message['replace']['id']
mathieui's avatar
mathieui committed
690 691
            if replaced_id is not '' and config.get_by_tabname(
                    'group_corrections', message['from'].bare):
692 693
                try:
                    delayed_date = date or datetime.now()
mathieui's avatar
mathieui committed
694 695 696 697
                    if tab.modify_message(
                            body,
                            replaced_id,
                            message['id'],
698
                            time=delayed_date,
mathieui's avatar
mathieui committed
699 700
                            nickname=nick_from,
                            user=user):
701 702 703 704
                        self.core.events.trigger('highlight', message, tab)
                    replaced = True
                except CorrectionError:
                    log.debug('Unable to correct a message', exc_info=True)
mathieui's avatar
mathieui committed
705 706 707 708 709 710 711 712
        if not replaced and tab.add_message(
                body,
                date,
                nick_from,
                history=delayed,
                identifier=message['id'],
                jid=message['from'],
                typ=1):
713
            self.core.events.trigger('highlight', message, tab)
mathieui's avatar
mathieui committed
714

715 716 717
        if message['from'].resource == tab.own_nick:
            tab.last_sent_message = message

718
        if tab is self.core.tabs.current_tab:
719
            tab.text_win.refresh()
720
            tab.info_header.refresh(tab, tab.text_win, user=tab.own_user)
721
            tab.input.refresh()
722
            self.core.doupdate()
723
        elif tab.state != old_state:
724
            self.core.refresh_tab_win()
725
            current = self.core.tabs.current_tab
726 727
            if hasattr(current, 'input') and current.input:
                current.input.refresh()
728
            self.core.doupdate()
729 730 731

        if 'message' in config.get('beep_on').split():
            if (not config.get_by_tabname('disable_beep', room_from)
732
                    and self.core.own_nick != message['from'].resource):
733 734 735 736
                curses.beep()

    def on_muc_own_nickchange(self, muc):
        "We changed our nick in a MUC"
737
        for tab in self.core.get_tabs(tabs.PrivateTab):
738 739 740
            if tab.parent_muc == muc:
                tab.own_nick = muc.own_nick

741
    def on_groupchat_private_message(self, message, sent):
742 743 744
        """
        We received a Private Message (from someone in a Muc)
        """
745 746 747
        jid = message['to'] if sent else message['from']
        with_nick = jid.resource
        if not with_nick:
748 749
            self.on_groupchat_message(message)
            return
750 751

        room_from = jid.bare
mathieui's avatar
mathieui committed
752
        use_xhtml = config.get_by_tabname('enable_xhtml_im', jid.bare)
753
        tmp_dir = get_image_cache()
mathieui's avatar
mathieui committed
754
        body = xhtml.get_body_from_message_stanza(
mathieui's avatar
mathieui committed
755
            message, use_xhtml=use_xhtml, extract_images_to=tmp_dir)
756
        tab = self.core.tabs.by_name_and_class(
mathieui's avatar
mathieui committed
757 758
            jid.full,
            tabs.PrivateTab)  # get the tab with the private conversation
759
        ignore = config.get_by_tabname('ignore_private', room_from)
mathieui's avatar
mathieui committed
760
        if not tab:  # It's the first message we receive: create the tab
761
            if body and not ignore:
762
                tab = self.core.open_private_window(room_from, with_nick,
mathieui's avatar
mathieui committed
763
                                                    False)
mathieui's avatar
mathieui committed
764 765
        sender_nick = (tab.own_nick
                       or self.core.own_nick) if sent else with_nick
766
        if ignore and not sent:
767
            self.core.events.trigger('ignored_private', message, tab)
768 769
            msg = config.get_by_tabname('private_auto_response', room_from)
            if msg and body:
mathieui's avatar
mathieui committed
770 771
                self.core.xmpp.send_message(
                    mto=jid.full, mbody=msg, mtype='chat')
772
            return
773
        self.core.events.trigger('private_msg', message, tab)
mathieui's avatar
mathieui committed
774
        body = xhtml.get_body_from_message_stanza(
mathieui's avatar
mathieui committed
775
            message, use_xhtml=use_xhtml, extract_images_to=tmp_dir)
776 777 778
        if not body or not tab:
            return
        replaced = False
779
        user = tab.parent_muc.get_user_by_name(with_nick)
mathieui's avatar
mathieui committed
780
        if message.xml.find('{urn:xmpp:message-correct:0}replace') is not None:
781
            replaced_id = message['replace']['id']
mathieui's avatar
mathieui committed
782 783
            if replaced_id is not '' and config.get_by_tabname(
                    'group_corrections', room_from):
784
                try:
mathieui's avatar
mathieui committed
785 786 787 788 789 790
                    tab.modify_message(
                        body,
                        replaced_id,
                        message['id'],
                        user=user,
                        jid=message['from'],
791
                        nickname=sender_nick)
792 793 794
                    replaced = True
                except CorrectionError:
                    log.debug('Unable to correct a message', exc_info=True)
795
        if not replaced:
mathieui's avatar
mathieui committed
796 797 798
            tab.add_message(
                body,
                time=None,
799 800
                nickname=sender_nick,
                nick_color=get_theme().COLOR_OWN_NICK if sent else None,
mathieui's avatar
mathieui committed
801 802 803 804
                forced_user=user,
                identifier=message['id'],
                jid=message['from'],
                typ=1)
805 806 807 808
        if sent:
            tab.last_sent_message = msg
        else:
            tab.last_remote_message = datetime.now()
809

810
        if not sent and 'private' in config.get('beep_on').split():
811 812
            if not config.get_by_tabname('disable_beep', jid.full):
                curses.beep()
813
        if tab is self.core.tabs.current_tab:
814
            self.core.refresh_window()
815
        else:
816
            tab.state = 'normal' if sent else 'private'
817
            self.core.refresh_tab_win()
mathieui's avatar
mathieui committed
818

819
    ### Chatstates ###
mathieui's avatar
mathieui committed
820

821
    def on_chatstate_active(self, message):
822
        self._on_chatstate(message, "active")
mathieui's avatar
mathieui committed
823

824
    def on_chatstate_inactive(self, message):
825
        self._on_chatstate(message, "inactive")
mathieui's avatar
mathieui committed
826

827
    def on_chatstate_composing(self, message):
828
        self._on_chatstate(message, "composing")
mathieui's avatar
mathieui committed
829

830
    def on_chatstate_paused(self, message):
831
        self._on_chatstate(message, "paused")
mathieui's avatar
mathieui committed
832

833
    def on_chatstate_gone(self, message):
834
        self._on_chatstate(message, "gone")
mathieui's avatar
mathieui committed
835

836
    def _on_chatstate(self, message, state):
837
        if message['type'] == 'chat':
838
            if not self._on_chatstate_normal_conversation(message, state):
839 840
                tab = self.core.tabs.by_name_and_class(message['from'].full,
                                                       tabs.PrivateTab)
841 842
                if not tab:
                    return
843
                self._on_chatstate_private_conversation(message, state)
844 845 846
        elif message['type'] == 'groupchat':
            self.on_chatstate_groupchat_conversation(message, state)

847 848
    def _on_chatstate_normal_conversation(self, message, state):
        tab = self.core.get_conversation_by_jid(message['from'], False)
849 850
        if not tab:
            return False
851
        self.core.events.trigger('normal_chatstate', message, tab)
852 853
        tab.chatstate = state
        if state == 'gone' and isinstance(tab, tabs.DynamicConversationTab):
mathieui's avatar
mathieui committed
854
            tab.unlock()
855
        if tab == self.core.tabs.current_tab:
856
            tab.refresh_info_header()
857
            self.core.doupdate()
858 859
        else:
            _composing_tab_state(tab, state)
860
            self.core.refresh_tab_win()
861
        return True
mathieui's avatar
mathieui committed
862

863
    def _on_chatstate_private_conversation(self, message, state):
864 865 866
        """
        Chatstate received in a private conversation from a MUC
        """
867 868
        tab = self.core.tabs.by_name_and_class(message['from'].full,
                                               tabs.PrivateTab)
869 870
        if not tab:
            return
871
        self.core.events.trigger('private_chatstate', message, tab)
872
        tab.chatstate = state
873
        if tab == self.core.tabs.current_tab:
874
            tab.refresh_info_header()
875
            self.core.doupdate()
mathieui's avatar
mathieui committed
876
        else:
877
            _composing_tab_state(tab, state)
878
            self.core.refresh_tab_win()
879 880 881 882 883 884 885

    def on_chatstate_groupchat_conversation(self, message, state):
        """
        Chatstate received in a MUC
        """
        nick = message['mucnick']
        room_from = message.get_mucroom()
886
        tab = self.core.tabs.by_name_and_class(room_from, tabs.MucTab)
887
        if tab and tab.get_user_by_name(nick):
888
            self.core.events.trigger('muc_chatstate', message, tab)
889
            tab.get_user_by_name(nick).chatstate = state
890
        if tab == self.core.tabs.current_tab:
891
            if not self.core.size.tab_degrade_x:
892 893
                tab.user_win.refresh(tab.users)
            tab.input.refresh()
894
            self.core.doupdate()
895 896
        else:
            _composing_tab_state(tab, state)
897
            self.core.refresh_tab_win()
mathieui's avatar
mathieui committed
898

899 900 901 902 903 904 905
    @staticmethod
    def _format_error(error):
            error_condition = error['condition']
            error_text = error['text']
            return '%s: %s' % (error_condition,
                               error_text) if error_text else error_condition