handlers.py 60 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, sha512
Link Mauve's avatar
Link Mauve committed
17
from os import path, makedirs
mathieui's avatar
mathieui committed
18

louiz’'s avatar
louiz’ committed
19
from slixmpp import InvalidJID
20 21
from slixmpp.xmlstream.stanzabase import StanzaBase, ElementBase
from xml.etree import ElementTree as ET
mathieui's avatar
mathieui committed
22

23 24 25 26 27 28 29 30
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.config import config, CACHE_DIR
31
from poezio.core.structs import Status
32 33 34 35 36
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
37

mathieui's avatar
mathieui committed
38
from poezio.core.commands import dumb_callback
mathieui's avatar
mathieui committed
39

40 41 42 43 44 45
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)
46
    PYGMENTS = True
47
except ImportError:
48
    PYGMENTS = False
49

50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
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.

SHA-512 of the old certificate: %s

SHA-512 of the new certificate: %s
"""

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.

"""

71 72 73 74 75 76 77 78 79 80 81 82 83

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
        """
        def callback(iq):
            if not iq:
                return
            features = iq['disco_info']['features']
84
            rostertab = self.core.get_tab_by_name('Roster', tabs.RosterInfoTab)
85 86 87 88
            rostertab.check_blocking(features)
            rostertab.check_saslexternal(features)
            if (config.get('enable_carbons') and
                    'urn:xmpp:carbons:2' in features):
89 90
                self.core.xmpp.plugin['xep_0280'].enable()
            self.core.check_bookmark_storage(features)
91

92 93
        self.core.xmpp.plugin['xep_0030'].get_info(jid=self.core.xmpp.boundjid.domain,
                                                   callback=callback)
94 95 96 97 98 99 100 101 102

    def on_carbon_received(self, message):
        """
        Carbon <received/> received
        """
        def ignore_message(recv):
            log.debug('%s has category conference, ignoring carbon',
                      recv['from'].server)
        def receive_message(recv):
103
            recv['to'] = self.core.xmpp.boundjid.full
104 105 106 107 108 109 110
            if recv['receipt']:
                return self.on_receipt(recv)
            self.on_normal_message(recv)

        recv = message['carbon_received']
        if (recv['from'].bare not in roster or
            roster[recv['from'].bare].subscription == 'none'):
111
            fixes.has_identity(self.core.xmpp, recv['from'].server,
112 113 114
                               identity='conference',
                               on_true=functools.partial(ignore_message, recv),
                               on_false=functools.partial(receive_message, recv))
mathieui's avatar
mathieui committed
115
            return
116 117 118 119 120 121 122 123 124 125 126
        else:
            receive_message(recv)

    def on_carbon_sent(self, message):
        """
        Carbon <sent/> received
        """
        def ignore_message(sent):
            log.debug('%s has category conference, ignoring carbon',
                      sent['to'].server)
        def send_message(sent):
127
            sent['from'] = self.core.xmpp.boundjid.full
128 129 130 131 132
            self.on_normal_message(sent)

        sent = message['carbon_sent']
        if (sent['to'].bare not in roster or
                roster[sent['to'].bare].subscription == 'none'):
133
            fixes.has_identity(self.core.xmpp, sent['to'].server,
134 135 136 137 138 139 140 141 142 143 144 145 146
                               identity='conference',
                               on_true=functools.partial(ignore_message, sent),
                               on_false=functools.partial(send_message, sent))
        else:
            send_message(sent)

    ### Invites ###

    def on_groupchat_invitation(self, message):
        """
        Mediated invitation received
        """
        jid = message['from']
147
        if jid.bare in self.core.pending_invites:
148 149
            return
        # there are 2 'x' tags in the messages, making message['x'] useless
150
        invite = StanzaBase(self.core.xmpp, xml=message.xml.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite'))
151 152 153 154 155 156 157 158
        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
159
        self.core.information(msg, 'Info')
160 161 162
        if 'invite' in config.get('beep_on').split():
            curses.beep()
        logger.log_roster_change(inviter.full, 'invited you to %s' % jid.full)
163
        self.core.pending_invites[jid.bare] = inviter.full
164 165 166 167 168 169 170 171 172 173

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

177 178 179 180 181 182 183 184 185 186 187 188 189
        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

190
        self.core.information(msg, 'Info')
191 192 193
        if 'invite' in config.get('beep_on').split():
            curses.beep()

194
        self.core.pending_invites[room.bare] = inviter.full
195 196 197 198 199 200 201 202 203
        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)
        """
204
        if message.xml.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite') is not None:
205 206 207 208 209
            return
        if message['type'] == 'groupchat':
            return
        # Differentiate both type of messages, and call the appropriate handler.
        jid_from = message['from']
210
        for tab in self.core.get_tabs(tabs.MucTab):
211 212
            if tab.name == jid_from.bare:
                if message['type'] == 'chat':
213 214 215
                    self.on_groupchat_private_message(message)
                    return
        self.on_normal_message(message)
216 217 218 219 220 221

    def on_error_message(self, message):
        """
        When receiving any message with type="error"
        """
        jid_from = message['from']
222
        for tab in self.core.get_tabs(tabs.MucTab):
223
            if tab.name == jid_from.bare:
224
                if jid_from.full == jid_from.bare:
225
                    self.core.room_error(message, jid_from.bare)
226
                else:
227 228 229 230 231 232
                    text = self.core.get_error_message(message)
                    p_tab = self.core.get_tab_by_name(jid_from.full, tabs.PrivateTab)
                    if p_tab:
                        p_tab.add_error(text)
                    else:
                        self.core.information(text, 'Error')
233 234 235
                return
        tab = self.core.get_conversation_by_jid(message['from'], create=False)
        error_msg = self.core.get_error_message(message, deprecated=True)
236
        if not tab:
237 238
            self.core.information(error_msg, 'Error')
            return
239 240 241 242
        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)
243
            self.core.refresh_window()
244 245 246 247 248 249 250 251 252 253


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

mathieui's avatar
mathieui committed
256
        use_xhtml = config.get_by_tabname('enable_xhtml_im', message['from'].bare)
257 258 259 260 261 262
        tmp_dir = config.get('tmp_image_dir') or path.join(CACHE_DIR, 'images')
        extract_images = config.get('extract_inline_images')
        body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml,
                                                  tmp_dir=tmp_dir,
                                                  extract_images=extract_images)
        if not body:
263 264 265
            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
266
            body = message['body']
mathieui's avatar
mathieui committed
267

268 269
        remote_nick = ''
        # normal message, we are the recipient
270
        if message['to'].bare == self.core.xmpp.boundjid.bare:
271 272 273 274 275 276 277 278 279 280
            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']
281
            if not remote_nick:
282 283 284 285 286
                remote_nick = conv_jid.user
                if not remote_nick:
                    remote_nick = conv_jid.full
            own = False
        # we wrote the message (happens with carbons)
287
        elif message['from'].bare == self.core.xmpp.boundjid.bare:
288
            conv_jid = message['to']
289
            jid = self.core.xmpp.boundjid
290
            color = get_theme().COLOR_OWN_NICK
291
            remote_nick = self.core.own_nick
292 293
            own = True
        # we are not part of that message, drop it
mathieui's avatar
mathieui committed
294
        else:
295 296
            return

297
        conversation = self.core.get_conversation_by_jid(conv_jid, create=True)
298 299 300 301 302
        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
303 304
        elif not own:
            remote_nick = conversation.get_nick()
305

306 307 308
        if not own:
            conversation.last_remote_message = datetime.now()

309
        self.core.events.trigger('conversation_msg', message, conversation)
310 311 312 313 314 315 316 317
        if not message['body']:
            return
        body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml,
                                                  tmp_dir=tmp_dir,
                                                  extract_images=extract_images)
        delayed, date = common.find_delayed_tag(message)

        def try_modify():
Link Mauve's avatar
Link Mauve committed
318
            if message.xml.find('{urn:xmpp:message-correct:0}replace') is None:
319
                return False
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
            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

        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 conversation.remote_wants_chatstates is None and not delayed:
            if message['chat_state']:
                conversation.remote_wants_chatstates = True
            else:
                conversation.remote_wants_chatstates = False
        if not own and 'private' in config.get('beep_on').split():
            if not config.get_by_tabname('disable_beep', conv_jid.bare):
                curses.beep()
348
        if self.core.current_tab() is not conversation:
349 350
            if not own:
                conversation.state = 'private'
351
                self.core.refresh_tab_win()
352 353
            else:
                conversation.set_state('normal')
354
                self.core.refresh_tab_win()
355
        else:
356
            self.core.refresh_window()
mathieui's avatar
mathieui committed
357

358 359 360 361 362 363 364 365 366 367
    @asyncio.coroutine
    def on_0084_avatar(self, msg):
        jid = msg['from'].bare
        contact = roster[jid]
        if not contact:
            return
        log.debug('Received 0084 avatar update from %s', jid)
        try:
            metadata = msg['pubsub_event']['items']['item']['avatar_metadata']['items']
        except Exception:
368
            log.debug('Failed getting metadata from 0084:', exc_info=True)
369
            return
Link Mauve's avatar
Link Mauve committed
370
        cache_dir = path.join(CACHE_DIR, 'avatars', jid)
371
        for info in metadata:
Link Mauve's avatar
Link Mauve committed
372 373 374 375 376 377 378 379 380 381 382 383 384
            avatar_hash = info['id']

            # First check whether we have it in cache.
            cached_path = path.join(cache_dir, avatar_hash)
            try:
                with open(cached_path, 'rb') as avatar_file:
                    contact.avatar = avatar_file.read()
                log.debug('Using cached avatar')
                return
            except OSError:
                pass

            # If we didn’t have any, query the data instead.
385 386 387
            if not info['url']:
                try:
                    result = yield from self.core.xmpp['xep_0084'].retrieve_avatar(jid,
Link Mauve's avatar
Link Mauve committed
388
                                                                                   avatar_hash,
389
                                                                                   timeout=60)
390 391
                    contact.avatar = result['pubsub']['items']['item']['avatar_data']['value']
                except Exception:
392
                    log.debug('Failed retrieving 0084 data from %s:', jid, exc_info=True)
Link Mauve's avatar
Link Mauve committed
393
                    continue
394 395
                log.debug('Received %s avatar: %s', jid, info['type'])

Link Mauve's avatar
Link Mauve committed
396 397 398 399 400 401 402 403 404
                # Now we save the data on the file system to not have to request it again.
                try:
                    makedirs(cache_dir)
                    with open(cached_path, 'wb') as avatar_file:
                        avatar_file.write(contact.avatar)
                except OSError:
                    pass
                return

405 406 407
    @asyncio.coroutine
    def on_vcard_avatar(self, pres):
        jid = pres['from'].bare
408 409 410
        contact = roster[jid]
        if not contact:
            return
Link Mauve's avatar
Link Mauve committed
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425
        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.
        cache_dir = path.join(CACHE_DIR, 'avatars', jid)
        cached_path = path.join(cache_dir, avatar_hash)
        try:
            with open(cached_path, 'rb') as avatar_file:
                contact.avatar = avatar_file.read()
            log.debug('Using cached avatar')
            return
        except OSError:
            pass

        # If we didn’t have any, query the vCard instead.
426 427 428
        try:
            result = yield from self.core.xmpp['xep_0054'].get_vcard(jid,
                                                                     cached=True,
429 430 431
                                                                     timeout=60)
            avatar = result['vcard_temp']['PHOTO']
            contact.avatar = avatar['BINVAL']
432
        except Exception:
433
            log.debug('Failed retrieving vCard from %s:', jid, exc_info=True)
434 435 436
            return
        log.debug('Received %s avatar: %s', jid, avatar['TYPE'])

Link Mauve's avatar
Link Mauve committed
437 438 439 440 441 442 443 444
        # Now we save the data on the file system to not have to request it again.
        try:
            makedirs(cache_dir)
            with open(cached_path, 'wb') as avatar_file:
                avatar_file.write(contact.avatar)
        except OSError:
            pass

445 446 447 448 449 450 451 452 453
    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']
454
        if item.xml.find('{http://jabber.org/protocol/nick}nick') is not None:
455
            contact.name = item['nick']['nick']
mathieui's avatar
mathieui committed
456
        else:
457 458 459 460 461 462 463 464 465 466 467 468
            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
469
        if item.xml.find('{urn:xmpp:gaming:0}gaming') is not None:
470 471 472 473 474 475 476 477 478 479 480
            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'],
                }
mathieui's avatar
mathieui committed
481
        else:
482
            contact.gaming = {}
mathieui's avatar
mathieui committed
483

484 485
        if contact.gaming:
            logger.log_roster_change(contact.bare_jid, 'is playing %s' % (common.format_gaming_string(contact.gaming)))
mathieui's avatar
mathieui committed
486

487 488
        if old_gaming != contact.gaming and config.get_by_tabname('display_gaming_notifications', contact.bare_jid):
            if contact.gaming:
489
                self.core.information('%s is playing %s' % (contact.bare_jid, common.format_gaming_string(contact.gaming)), 'Gaming')
490
            else:
491
                self.core.information(contact.bare_jid + ' stopped playing.', 'Gaming')
492 493 494 495 496 497 498 499 500 501 502 503

    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
504
        if item.xml.find('{http://jabber.org/protocol/mood}mood') is not None:
505 506 507 508 509 510 511 512 513
            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
514
        else:
515 516 517 518 519 520 521
            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:
522
                self.core.information('Mood from '+ contact.bare_jid + ': ' + contact.mood, 'Mood')
523
            else:
524
                self.core.information(contact.bare_jid + ' stopped having his/her mood.', 'Mood')
525 526 527 528 529 530 531 532

    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
533
            return
534 535 536
        roster.modified()
        item = message['pubsub_event']['items']['item']
        old_activity = contact.activity
537
        if item.xml.find('{http://jabber.org/protocol/activity}activity') is not None:
538 539 540 541 542 543 544 545 546 547 548 549 550 551 552
            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
553 554 555 556
        else:
            contact.activity = ''

        if contact.activity:
557 558 559 560
            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:
561
                self.core.information('Activity from '+ contact.bare_jid + ': ' + contact.activity, 'Activity')
562
            else:
563
                self.core.information(contact.bare_jid + ' stopped doing his/her activity.', 'Activity')
564 565 566 567 568 569 570 571 572 573 574 575

    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
576
        if item.xml.find('{http://jabber.org/protocol/tune}tune') is not None:
577 578 579 580 581 582 583 584 585 586
            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']
                }
mathieui's avatar
mathieui committed
587
        else:
588 589
            contact.tune = {}

mathieui's avatar
mathieui committed
590
        if contact.tune:
591
            logger.log_roster_change(message['from'].bare, 'is now listening to %s' % common.format_tune_string(contact.tune))
mathieui's avatar
mathieui committed
592

593 594
        if old_tune != contact.tune and config.get_by_tabname('display_tune_notifications', contact.bare_jid):
            if contact.tune:
595
                self.core.information(
596 597 598
                        'Tune from '+ message['from'].bare + ': ' + common.format_tune_string(contact.tune),
                        'Tune')
            else:
599
                self.core.information(contact.bare_jid + ' stopped listening to music.', 'Tune')
mathieui's avatar
mathieui committed
600

601 602 603 604 605 606 607
    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
608

609
        if message['type'] == 'error': # Check if it's an error
610 611
            self.core.room_error(message, room_from)
            return
mathieui's avatar
mathieui committed
612

613
        tab = self.core.get_tab_by_name(room_from, tabs.MucTab)
614
        if not tab:
615 616
            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='')
617
            return
mathieui's avatar
mathieui committed
618

619 620 621 622
        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
623

624
        self.core.events.trigger('muc_msg', message, tab)
mathieui's avatar
mathieui committed
625
        use_xhtml = config.get_by_tabname('enable_xhtml_im', room_from)
626 627 628 629 630 631 632
        tmp_dir = config.get('tmp_image_dir') or path.join(CACHE_DIR, 'images')
        extract_images = config.get('extract_inline_images')
        body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml,
                                                  tmp_dir=tmp_dir,
                                                  extract_images=extract_images)
        if not body:
            return
mathieui's avatar
mathieui committed
633

634 635 636
        old_state = tab.state
        delayed, date = common.find_delayed_tag(message)
        replaced = False
mathieui's avatar
mathieui committed
637
        if message.xml.find('{urn:xmpp:message-correct:0}replace') is not None:
638 639 640 641 642 643 644 645 646 647 648 649
            replaced_id = message['replace']['id']
            if replaced_id is not '' 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)
650
        if not replaced and tab.add_message(body, date, nick_from, history=delayed, identifier=message['id'], jid=message['from'], typ=1):
651
            self.core.events.trigger('highlight', message, tab)
mathieui's avatar
mathieui committed
652

653 654 655
        if message['from'].resource == tab.own_nick:
            tab.last_sent_message = message

656
        if tab is self.core.current_tab():
657
            tab.text_win.refresh()
658
            tab.info_header.refresh(tab, tab.text_win, user=tab.own_user)
659
            tab.input.refresh()
660
            self.core.doupdate()
661
        elif tab.state != old_state:
662 663
            self.core.refresh_tab_win()
            current = self.core.current_tab()
664 665
            if hasattr(current, 'input') and current.input:
                current.input.refresh()
666
            self.core.doupdate()
667 668 669

        if 'message' in config.get('beep_on').split():
            if (not config.get_by_tabname('disable_beep', room_from)
670
                    and self.core.own_nick != message['from'].resource):
671 672 673 674
                curses.beep()

    def on_muc_own_nickchange(self, muc):
        "We changed our nick in a MUC"
675
        for tab in self.core.get_tabs(tabs.PrivateTab):
676 677 678 679 680 681 682 683 684 685
            if tab.parent_muc == muc:
                tab.own_nick = muc.own_nick

    def on_groupchat_private_message(self, message):
        """
        We received a Private Message (from someone in a Muc)
        """
        jid = message['from']
        nick_from = jid.resource
        if not nick_from:
686 687
            self.on_groupchat_message(message)
            return
688 689

        room_from = jid.bare
mathieui's avatar
mathieui committed
690
        use_xhtml = config.get_by_tabname('enable_xhtml_im', jid.bare)
691 692 693 694 695
        tmp_dir = config.get('tmp_image_dir') or path.join(CACHE_DIR, 'images')
        extract_images = config.get('extract_inline_images')
        body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml,
                                                  tmp_dir=tmp_dir,
                                                  extract_images=extract_images)
696
        tab = self.core.get_tab_by_name(jid.full, tabs.PrivateTab) # get the tab with the private conversation
697 698 699
        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:
700
                tab = self.core.open_private_window(room_from, nick_from, False)
701
        if ignore:
702
            self.core.events.trigger('ignored_private', message, tab)
703 704
            msg = config.get_by_tabname('private_auto_response', room_from)
            if msg and body:
705
                self.core.xmpp.send_message(mto=jid.full, mbody=msg, mtype='chat')
706
            return
707
        tab.last_remote_message = datetime.now()
708
        self.core.events.trigger('private_msg', message, tab)
709 710 711 712 713 714
        body = xhtml.get_body_from_message_stanza(message, use_xhtml=use_xhtml,
                                                  tmp_dir=tmp_dir,
                                                  extract_images=extract_images)
        if not body or not tab:
            return
        replaced = False
715
        user = tab.parent_muc.get_user_by_name(nick_from)
mathieui's avatar
mathieui committed
716
        if message.xml.find('{urn:xmpp:message-correct:0}replace') is not None:
717 718 719 720 721 722 723 724 725
            replaced_id = message['replace']['id']
            if replaced_id is not '' and config.get_by_tabname('group_corrections',
                                                               room_from):
                try:
                    tab.modify_message(body, replaced_id, message['id'], user=user, jid=message['from'],
                            nickname=nick_from)
                    replaced = True
                except CorrectionError:
                    log.debug('Unable to correct a message', exc_info=True)
726 727 728 729 730 731 732 733 734 735
        if not replaced:
            tab.add_message(body, time=None, nickname=nick_from,
                            forced_user=user,
                            identifier=message['id'],
                            jid=message['from'],
                            typ=1)

        if tab.remote_wants_chatstates is None:
            if message['chat_state']:
                tab.remote_wants_chatstates = True
mathieui's avatar
mathieui committed
736
            else:
737 738 739 740
                tab.remote_wants_chatstates = False
        if 'private' in config.get('beep_on').split():
            if not config.get_by_tabname('disable_beep', jid.full):
                curses.beep()
741 742
        if tab is self.core.current_tab():
            self.core.refresh_window()
743 744
        else:
            tab.state = 'private'
745
            self.core.refresh_tab_win()
mathieui's avatar
mathieui committed
746

747
    ### Chatstates ###
mathieui's avatar
mathieui committed
748

749
    def on_chatstate_active(self, message):
750
        self._on_chatstate(message, "active")
mathieui's avatar
mathieui committed
751

752
    def on_chatstate_inactive(self, message):
753
        self._on_chatstate(message, "inactive")
mathieui's avatar
mathieui committed
754

755
    def on_chatstate_composing(self, message):
756
        self._on_chatstate(message, "composing")
mathieui's avatar
mathieui committed
757

758
    def on_chatstate_paused(self, message):
759
        self._on_chatstate(message, "paused")
mathieui's avatar
mathieui committed
760

761
    def on_chatstate_gone(self, message):
762
        self._on_chatstate(message, "gone")
mathieui's avatar
mathieui committed
763

764
    def _on_chatstate(self, message, state):
765
        if message['type'] == 'chat':
766 767
            if not self._on_chatstate_normal_conversation(message, state):
                tab = self.core.get_tab_by_name(message['from'].full, tabs.PrivateTab)
768 769
                if not tab:
                    return
770
                self._on_chatstate_private_conversation(message, state)
771 772 773
        elif message['type'] == 'groupchat':
            self.on_chatstate_groupchat_conversation(message, state)

774 775
    def _on_chatstate_normal_conversation(self, message, state):
        tab = self.core.get_conversation_by_jid(message['from'], False)
776 777 778
        if not tab:
            return False
        tab.remote_wants_chatstates = True
779
        self.core.events.trigger('normal_chatstate', message, tab)
780 781
        tab.chatstate = state
        if state == 'gone' and isinstance(tab, tabs.DynamicConversationTab):
mathieui's avatar
mathieui committed
782
            tab.unlock()
783
        if tab == self.core.current_tab():
784
            tab.refresh_info_header()
785
            self.core.doupdate()
786 787
        else:
            _composing_tab_state(tab, state)
788
            self.core.refresh_tab_win()
789
        return True
mathieui's avatar
mathieui committed
790

791
    def _on_chatstate_private_conversation(self, message, state):
792 793 794
        """
        Chatstate received in a private conversation from a MUC
        """
795
        tab = self.core.get_tab_by_name(message['from'].full, tabs.PrivateTab)
796 797 798
        if not tab:
            return
        tab.remote_wants_chatstates = True
799
        self.core.events.trigger('private_chatstate', message, tab)
800
        tab.chatstate = state
801
        if tab == self.core.current_tab():
802
            tab.refresh_info_header()
803
            self.core.doupdate()
mathieui's avatar
mathieui committed
804
        else:
805
            _composing_tab_state(tab, state)
806
            self.core.refresh_tab_win()
807 808 809 810 811 812 813

    def on_chatstate_groupchat_conversation(self, message, state):
        """
        Chatstate received in a MUC
        """
        nick = message['mucnick']
        room_from = message.get_mucroom()
814
        tab = self.core.get_tab_by_name(room_from, tabs.MucTab)
815
        if tab and tab.get_user_by_name(nick):
816
            self.core.events.trigger('muc_chatstate', message, tab)
817
            tab.get_user_by_name(nick).chatstate = state
818 819
        if tab == self.core.current_tab():
            if not self.core.size.tab_degrade_x:
820 821
                tab.user_win.refresh(tab.users)
            tab.input.refresh()
822
            self.core.doupdate()
823 824
        else:
            _composing_tab_state(tab, state)
825
            self.core.refresh_tab_win()
mathieui's avatar
mathieui committed
826

827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844
    ### subscription-related handlers ###

    def on_roster_update(self, iq):
        """
        The roster was received.
        """
        for item in iq['roster']:
            try:
                jid = item['jid']
            except InvalidJID:
                jid = item._get_attr('jid', '')
                log.error('Invalid JID: "%s"', jid, exc_info=True)
            else:
                if item['subscription'] == 'remove':
                    del roster[jid]
                else:
                    roster.update_contact_groups(jid)
        roster.update_size()
845 846
        if isinstance(self.core.current_tab(), tabs.RosterInfoTab):
            self.core.refresh_window()
mathieui's avatar
mathieui committed
847

848 849 850 851 852 853 854
    def on_subscription_request(self, presence):
        """subscribe received"""
        jid = presence['from'].bare
        contact = roster[jid]
        if contact and contact.subscription in ('from', 'both'):
            return
        elif contact and contact.subscription == 'to':
855 856
            self.core.xmpp.send_presence(pto=jid, ptype='subscribed')
            self.core.xmpp.send_presence(pto=jid)
louiz’'s avatar
louiz’ committed
857
        else:
858 859 860 861
            if not contact:
                contact = roster.get_and_set(jid)
            roster.update_contact_groups(contact)
            contact.pending_in = True
862 863 864 865 866
            self.core.information('%s wants to subscribe to your presence, use '
                                  '/accept <jid> or /deny <jid> in the roster '
                                  'tab to accept or reject the query.' % jid,
                                  'Roster')
            self.core.get_tab_by_number(0).state = 'highlight'
867
            roster.modified()
868 869
        if isinstance(self.core.current_tab(), tabs.RosterInfoTab):
            self.core.refresh_window()
mathieui's avatar
mathieui committed
870

871 872 873 874 875
    def on_subscription_authorized(self, presence):
        """subscribed received"""
        jid = presence['from'].bare
        contact = roster[jid]
        if contact.subscription not in ('both', 'from'):
876
            self.core.information('%s accepted your contact proposal' % jid, 'Roster')
877 878 879 880 881
        if contact.pending_out:
            contact.pending_out = False

        roster.modified()

882 883
        if isinstance(self.core.current_tab(), tabs.RosterInfoTab):
            self.core.refresh_window()
884 885 886 887 888 889

    def on_subscription_remove(self, presence):
        """unsubscribe received"""
        jid = presence['from'].bare
        contact = roster[jid]
        if not contact:
mathieui's avatar
mathieui committed
890
            return
891
        roster.modified()
892 893 894 895
        self.core.information('%s does not want to receive your status anymore.' % jid, 'Roster')
        self.core.get_tab_by_number(0).state = 'highlight'
        if isinstance(self.core.current_tab(), tabs.RosterInfoTab):
            self.core.refresh_window()
896 897 898 899 900 901

    def on_subscription_removed(self, presence):
        """unsubscribed received"""
        jid = presence['from'].bare
        contact = roster[jid]
        if not contact:
mathieui's avatar
mathieui committed
902
            return
903 904
        roster.modified()
        if contact.pending_out:
905
            self.core.information('%s rejected your contact proposal' % jid, 'Roster')
906
            contact.pending_out = False
907
        else:
908 909 910 911
            self.core.information('%s does not want you to receive his/her/its status anymore.'%jid, 'Roster')
        self.core.get_tab_by_number(0).state = 'highlight'
        if isinstance(self.core.current_tab(), tabs.RosterInfoTab):
            self.core.refresh_window()
912 913 914 915

    ### Presence-related handlers ###

    def on_presence(self, presence):
916
        if presence.match('presence/muc') or presence.xml.find('{http://jabber.org/protocol/muc#user}x') is not None:
917 918 919
            return
        jid = presence['from']
        contact = roster[jid.bare]
920
        tab = self.core.get_conversation_by_jid(jid, create=False)
921 922 923 924 925 926 927 928 929
        if isinstance(tab, tabs.DynamicConversationTab):
            if tab.get_dest_jid() != jid.full:
                tab.unlock(from_=jid.full)
            elif presence['type'] == 'unavailable':
                tab.unlock()
        if contact is None:
            return
        roster.modified()
        contact.error = None
930 931
        self.core.events.trigger('normal_presence', presence, contact[jid.full])
        tab = self.core.get_conversation_by_jid(jid, create=False)
932 933
        if tab:
            tab.update_status(Status(show=presence['show'], message=presence['status']))
934 935 936
        if isinstance(self.core.current_tab(), tabs.RosterInfoTab):
            self.core.refresh_window()
        elif self.core.current_tab() == tab:
937
            tab.refresh()
938
            self.core.doupdate()
mathieui's avatar
mathieui committed
939

940 941 942 943 944 945 946 947
    def on_presence_error(self, presence):
        jid = presence['from']
        contact = roster[jid.bare]
        if not contact:
            return
        roster.modified()
        contact.error = presence['error']['type'] + ': ' + presence['error']['condition']
        # reset chat states status on presence error
948
        tab = self.core.get_tab_by_name(jid.full, tabs.ConversationTab)
949 950 951 952 953 954 955
        if tab:
            tab.remote_wants_chatstates = None

    def on_got_offline(self, presence):
        """
        A JID got offline
        """
956
        if presence.match('presence/muc') or presence.xml.find('{http://jabber.org/protocol/muc#user}x') is not None:
957 958 959
            return
        jid = presence['from']
        if not logger.log_roster_change(jid.bare, 'got offline'):
960
            self.core.information('Unable to write in the log file', 'Error')
961 962 963 964 965 966 967 968 969
        # If a resource got offline, display the message in the conversation with this
        # precise resource.
        contact = roster[jid.bare]
        name = jid.bare
        if contact:
            roster.connected -= 1
            if contact.name:
                name = contact.name
        if jid.resource:
970 971 972
            self.core.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x191}offline' % name)
        self.core.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x191}offline' % name)
        self.core.information('\x193}%s \x195}is \x191}offline' % name, 'Roster')
973
        roster.modified()
974 975
        if isinstance(self.core.current_tab(), tabs.RosterInfoTab):
            self.core.refresh_window()
976 977 978 979 980

    def on_got_online(self, presence):
        """
        A JID got online
        """
981
        if presence.match('presence/muc') or presence.xml.find('{http://jabber.org/protocol/muc#user}x') is not None:
982 983 984 985 986 987 988 989 990
            return
        jid = presence['from']
        contact = roster[jid.bare]
        if contact is None:
            # Todo, handle presence coming from contacts not in roster
            return
        roster.connected += 1
        roster.modified()
        if not logger.log_roster_change(jid.bare, 'got online'):
991
            self.core.information('Unable to write in the log file', 'Error')
992 993 994 995 996
        resource = Resource(jid.full, {
            'priority': presence.get_priority() or 0,
            'status': presence['status'],
            'show': presence['show'],
            })
997
        self.core.events.trigger('normal_presence', presence, resource)
998
        name = contact.name if contact.name else jid.bare
999 1000
        self.core.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x194}online' % name)
        if time.time() - self.core.connection_time > 10:
1001 1002
            # We do not display messages if we recently logged in
            if presence['status']:
1003
                self.core.information("\x193}%s \x195}is \x194}online\x195} (\x19o%s\x195})" % (name, presence['status']), "Roster")
1004
            else:
1005 1006 1007 1008
                self.core.information("\x193}%s \x195}is \x194}online\x195}" % name, "Roster")
            self.core.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x194}online' % name)
        if isinstance(self.core.current_tab(), tabs.RosterInfoTab):
            self.core.refresh_window()
1009 1010 1011 1012 1013 1014 1015 1016

    def on_groupchat_presence(self, 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
        presence information of the concerned user
        """
        from_room = presence['from'].bare
1017
        tab = self.core.get_tab_by_name(from_room, tabs.MucTab)
1018
        if tab: