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

906
907
908
909
910
911
    def on_version_result(self