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

import logging
log = logging.getLogger(__name__)

8
9
from typing import Optional

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

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

28
29
30
31
32
33
34
from poezio import common
from poezio import fixes
from poezio import pep
from poezio import tabs
from poezio import xhtml
from poezio import multiuserchat as muc
from poezio.common import safeJID
35
from poezio.config import config, get_image_cache
36
from poezio.core.structs import Status
37
38
39
40
41
from poezio.contact import Resource
from poezio.logger import logger
from poezio.roster import roster
from poezio.text_buffer import CorrectionError, AckError
from poezio.theming import dump_tuple, get_theme
mathieui's avatar
mathieui committed
42

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

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

55
56
57
58
59
60
61
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.

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

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

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.

"""

76
77
78
79
80
81
82
83
84

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
85

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

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

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

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

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

        jid_bare = with_jid.bare

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

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

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

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

153
154
        return None

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

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

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

        recv = message['carbon_received']
171
172
173
174
175
        is_muc_pm = self.is_known_muc_pm(recv, recv['from'])
        if is_muc_pm:
            log.debug('%s sent a MUC-PM, ignoring carbon', recv['from'])
            return
        if is_muc_pm is None:
mathieui's avatar
mathieui committed
176
177
            fixes.has_identity(
                self.core.xmpp,
178
                recv['from'].bare,
mathieui's avatar
mathieui committed
179
180
181
                identity='conference',
                on_true=functools.partial(ignore_message, recv),
                on_false=functools.partial(receive_message, recv))
mathieui's avatar
mathieui committed
182
            return
183
184
185
186
187
188
189
        else:
            receive_message(recv)

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

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

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

        sent = message['carbon_sent']
199
200
201
202
203
        is_muc_pm = self.is_known_muc_pm(sent, sent['to'])
        if is_muc_pm:
            groupchat_private_message(sent)
            return
        if is_muc_pm is None:
mathieui's avatar
mathieui committed
204
205
206
207
            fixes.has_identity(
                self.core.xmpp,
                sent['to'].server,
                identity='conference',
208
                on_true=functools.partial(groupchat_private_message, sent),
mathieui's avatar
mathieui committed
209
                on_false=functools.partial(send_message, sent))
210
211
212
213
214
215
216
217
218
219
        else:
            send_message(sent)

    ### Invites ###

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

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

256
257
258
259
260
261
262
263
264
265
266
267
268
        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

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

273
        self.core.pending_invites[room.bare] = inviter.full
274
275
276
277
278
279
280
281
282
        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
283
284
285
        if message.xml.find(
                '{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite'
        ) is not None:
286
287
288
289
            return
        if message['type'] == 'groupchat':
            return
        # Differentiate both type of messages, and call the appropriate handler.
290
291
292
        if self.is_known_muc_pm(message, message['from']):
            self.on_groupchat_private_message(message, sent=False)
            return
293
        self.on_normal_message(message)
294

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

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

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

mathieui's avatar
mathieui committed
343
344
        use_xhtml = config.get_by_tabname('enable_xhtml_im',
                                          message['from'].bare)
345
        tmp_dir = get_image_cache()
mathieui's avatar
mathieui committed
346
        body = xhtml.get_body_from_message_stanza(
mathieui's avatar
mathieui committed
347
            message, use_xhtml=use_xhtml, extract_images_to=tmp_dir)
348
        if not body:
349
350
351
            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
352
            body = message['body']
mathieui's avatar
mathieui committed
353

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

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

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

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

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

        def try_modify():
Link Mauve's avatar
Link Mauve committed
405
            if message.xml.find('{urn:xmpp:message-correct:0}replace') is None:
406
                return False
407
408
409
410
            replaced_id = message['replace']['id']
            if replaced_id and config.get_by_tabname('group_corrections',
                                                     conv_jid.bare):
                try:
mathieui's avatar
mathieui committed
411
412
413
414
415
416
                    conversation.modify_message(
                        body,
                        replaced_id,
                        message['id'],
                        jid=jid,
                        nickname=remote_nick)
417
418
419
420
421
422
                    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
423
424
425
426
427
428
429
430
431
            conversation.add_message(
                body,
                date,
                nickname=remote_nick,
                nick_color=color,
                history=delayed,
                identifier=message['id'],
                jid=jid,
                typ=1)
432
433
434
435

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

446
    async def on_0084_avatar(self, msg):
447
448
449
450
451
452
        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
453
454
            metadata = msg['pubsub_event']['items']['item']['avatar_metadata'][
                'items']
455
        except Exception:
456
            log.debug('Failed getting metadata from 0084:', exc_info=True)
457
458
            return
        for info in metadata:
Link Mauve's avatar
Link Mauve committed
459
460
461
            avatar_hash = info['id']

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

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

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

496
    async def on_vcard_avatar(self, pres):
497
        jid = pres['from'].bare
498
499
500
        contact = roster[jid]
        if not contact:
            return
Link Mauve's avatar
Link Mauve committed
501
502
503
504
        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
505
506
        cached_avatar = self.core.avatar_cache.retrieve_by_jid(
            jid, avatar_hash)
507
508
509
        if cached_avatar:
            contact.avatar = cached_avatar
            log.debug('Using cached avatar for %s', jid)
Link Mauve's avatar
Link Mauve committed
510
511
512
            return

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

Link Mauve's avatar
Link Mauve committed
526
        # Now we save the data on the file system to not have to request it again.
mathieui's avatar
mathieui committed
527
528
529
530
        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
531

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

571
        if contact.gaming:
mathieui's avatar
mathieui committed
572
573
574
            logger.log_roster_change(
                contact.bare_jid, 'is playing %s' %
                (common.format_gaming_string(contact.gaming)))
mathieui's avatar
mathieui committed
575

mathieui's avatar
mathieui committed
576
577
        if old_gaming != contact.gaming and config.get_by_tabname(
                'display_gaming_notifications', contact.bare_jid):
578
            if contact.gaming:
mathieui's avatar
mathieui committed
579
                self.core.information(
mathieui's avatar
mathieui committed
580
581
582
                    '%s is playing %s' % (contact.bare_jid,
                                          common.format_gaming_string(
                                              contact.gaming)), 'Gaming')
583
            else:
mathieui's avatar
mathieui committed
584
585
                self.core.information(contact.bare_jid + ' stopped playing.',
                                      'Gaming')
586
587
588

    def on_mood_event(self, message):
        """
Maxime Buquet's avatar
Maxime Buquet committed
589
        Called when a pep notification for a user mood
590
591
592
593
594
595
596
597
        is received.
        """
        contact = roster[message['from'].bare]
        if not contact:
            return
        roster.modified()
        item = message['pubsub_event']['items']['item']
        old_mood = contact.mood
598
        if item.xml.find('{http://jabber.org/protocol/mood}mood') is not None:
599
600
601
602
603
604
605
606
607
            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
608
        else:
609
610
611
            contact.mood = ''

        if contact.mood:
mathieui's avatar
mathieui committed
612
613
            logger.log_roster_change(contact.bare_jid,
                                     'has now the mood: %s' % contact.mood)
614

mathieui's avatar
mathieui committed
615
616
        if old_mood != contact.mood and config.get_by_tabname(
                'display_mood_notifications', contact.bare_jid):
617
            if contact.mood:
mathieui's avatar
mathieui committed
618
619
620
                self.core.information(
                    'Mood from ' + contact.bare_jid + ': ' + contact.mood,
                    'Mood')
621
            else:
mathieui's avatar
mathieui committed
622
                self.core.information(
Kim Alvefur's avatar
Kim Alvefur committed
623
                    contact.bare_jid + ' stopped having their mood.', 'Mood')
624
625
626

    def on_activity_event(self, message):
        """
Maxime Buquet's avatar
Maxime Buquet committed
627
        Called when a pep notification for a user activity
628
629
630
631
        is received.
        """
        contact = roster[message['from'].bare]
        if not contact:
mathieui's avatar
mathieui committed
632
            return
633
634
635
        roster.modified()
        item = message['pubsub_event']['items']['item']
        old_activity = contact.activity
mathieui's avatar
mathieui committed
636
637
        if item.xml.find(
                '{http://jabber.org/protocol/activity}activity') is not None:
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
            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
653
654
655
656
        else:
            contact.activity = ''

        if contact.activity:
mathieui's avatar
mathieui committed
657
658
            logger.log_roster_change(
                contact.bare_jid, 'has now the activity %s' % contact.activity)
659

mathieui's avatar
mathieui committed
660
661
        if old_activity != contact.activity and config.get_by_tabname(
                'display_activity_notifications', contact.bare_jid):
662
            if contact.activity:
mathieui's avatar
mathieui committed
663
664
665
                self.core.information(
                    'Activity from ' + contact.bare_jid + ': ' +
                    contact.activity, 'Activity')
666
            else:
mathieui's avatar
mathieui committed
667
                self.core.information(
Kim Alvefur's avatar
Kim Alvefur committed
668
                    contact.bare_jid + ' stopped doing their activity.',
mathieui's avatar
mathieui committed
669
                    'Activity')
670
671
672

    def on_tune_event(self, message):
        """
Maxime Buquet's avatar
Maxime Buquet committed
673
        Called when a pep notification for a user tune
674
675
676
677
678
679
680
681
        is received
        """
        contact = roster[message['from'].bare]
        if not contact:
            return
        roster.modified()
        item = message['pubsub_event']['items']['item']
        old_tune = contact.tune
682
        if item.xml.find('{http://jabber.org/protocol/tune}tune') is not None:
683
684
            item = item['tune']
            contact.tune = {
mathieui's avatar
mathieui committed
685
686
687
688
689
690
691
692
                '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
693
        else:
694
695
            contact.tune = {}

mathieui's avatar
mathieui committed
696
        if contact.tune:
mathieui's avatar
mathieui committed
697
698
699
            logger.log_roster_change(
                message['from'].bare, 'is now listening to %s' %
                common.format_tune_string(contact.tune))
mathieui's avatar
mathieui committed
700

mathieui's avatar
mathieui committed
701
702
        if old_tune != contact.tune and config.get_by_tabname(
                'display_tune_notifications', contact.bare_jid):
703
            if contact.tune:
704
                self.core.information(
mathieui's avatar
mathieui committed
705
706
                    'Tune from ' + message['from'].bare + ': ' +
                    common.format_tune_string(contact.tune), 'Tune')
707
            else:
mathieui's avatar
mathieui committed
708
709
                self.core.information(
                    contact.bare_jid + ' stopped listening to music.', 'Tune')
mathieui's avatar
mathieui committed
710

711
712
713
714
715
    def on_groupchat_message(self, message):
        """
        Triggered whenever a message is received from a multi-user chat room.
        """
        room_from = message['from'].bare
mathieui's avatar
mathieui committed
716

mathieui's avatar
mathieui committed
717
        if message['type'] == 'error':  # Check if it's an error
718
719
            self.core.room_error(message, room_from)
            return
mathieui's avatar
mathieui committed
720

721
        tab = self.core.tabs.by_name_and_class(room_from, tabs.MucTab)
722
        if not tab:
mathieui's avatar
mathieui committed
723
724
725
726
            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='')
727
            return
mathieui's avatar
mathieui committed
728

729
730
731
732
        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
733

734
        self.core.events.trigger('muc_msg', message, tab)
mathieui's avatar
mathieui committed
735
        use_xhtml = config.get_by_tabname('enable_xhtml_im', room_from)
736
        tmp_dir = get_image_cache()
mathieui's avatar
mathieui committed
737
        body = xhtml.get_body_from_message_stanza(
mathieui's avatar
mathieui committed
738
            message, use_xhtml=use_xhtml, extract_images_to=tmp_dir)
Maxime Buquet's avatar
Maxime Buquet committed
739
740
741
742
743
744
745
746
747

        # TODO: #3314. Is this a MUC reflection?
        # Is this an encrypted message? Is so ignore.
        #   It is not possible in the OMEMO case to decrypt these messages
        #   since we don't encrypt for our own device (something something
        #   forward secrecy), but even for non-FS encryption schemes anyway
        #   messages shouldn't have changed after a round-trip to the room.
        # Otherwire replace the matching message we sent.

748
749
        if not body:
            return
mathieui's avatar
mathieui committed
750

751
752
753
        old_state = tab.state
        delayed, date = common.find_delayed_tag(message)
        replaced = False
mathieui's avatar
mathieui committed
754
        if message.xml.find('{urn:xmpp:message-correct:0}replace') is not None:
755
            replaced_id = message['replace']['id']
756
            if replaced_id != '' and config.get_by_tabname(
mathieui's avatar
mathieui committed
757
                    'group_corrections', message['from'].bare):
758
759
                try:
                    delayed_date = date or datetime.now()
mathieui's avatar
mathieui committed
760
761
762
763
                    if tab.modify_message(
                            body,
                            replaced_id,
                            message['id'],
764
                            time=delayed_date,
mathieui's avatar
mathieui committed
765
766
                            nickname=nick_from,
                            user=user):
767
768
769
770
                        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
771
772
773
774
775
776
777
778
        if not replaced and tab.add_message(
                body,
                date,
                nick_from,
                history=delayed,
                identifier=message['id'],
                jid=message['from'],
                typ=1):
779
            self.core.events.trigger('highlight', message, tab)
mathieui's avatar
mathieui committed
780

781
        if message['from'].resource == tab.own_nick:
Maxime Buquet's avatar
Maxime Buquet committed
782
            tab.set_last_sent_message(message, correct=replaced)
783

784
        if tab is self.core.tabs.current_tab:
785
            tab.text_win.refresh()
786
            tab.info_header.refresh(tab, tab.text_win, user=tab.own_user)
787
            tab.input.refresh()
788
            self.core.doupdate()
789
        elif tab.state != old_state:
790
            self.core.refresh_tab_win()
791
            current = self.core.tabs.current_tab
792
793
            if hasattr(current, 'input') and current.input:
                current.input.refresh()
794
            self.core.doupdate()
795
796
797

        if 'message' in config.get('beep_on').split():
            if (not config.get_by_tabname('disable_beep', room_from)
798
                    and self.core.own_nick != message['from'].resource):
799
800
801
802
                curses.beep()

    def on_muc_own_nickchange(self, muc):
        "We changed our nick in a MUC"
803
        for tab in self.core.get_tabs(tabs.PrivateTab):
804
805
806
            if tab.parent_muc == muc:
                tab.own_nick = muc.own_nick

807
    def on_groupchat_private_message(self, message, sent):
808
809
810
        """
        We received a Private Message (from someone in a Muc)
        """
811
812
813
        jid = message['to'] if sent else message['from']
        with_nick = jid.resource
        if not with_nick:
814
815
            self.on_groupchat_message(message)
            return
816
817

        room_from = jid.bare
mathieui's avatar
mathieui committed
818
        use_xhtml = config.get_by_tabname('enable_xhtml_im', jid.bare)
819
        tmp_dir = get_image_cache()
mathieui's avatar
mathieui committed
820
        body = xhtml.get_body_from_message_stanza(
mathieui's avatar
mathieui committed
821
            message, use_xhtml=use_xhtml, extract_images_to=tmp_dir)
822
        tab = self.core.tabs.by_name_and_class(
mathieui's avatar
mathieui committed
823
824
            jid.full,
            tabs.PrivateTab)  # get the tab with the private conversation
825
        ignore = config.get_by_tabname('ignore_private', room_from)
mathieui's avatar
mathieui committed
826
        if not tab:  # It's the first message we receive: create the tab
827
            if body and not ignore:
828
                tab = self.core.open_private_window(room_from, with_nick,
mathieui's avatar
mathieui committed
829
                                                    False)
830
831
832
        # Tab can still be None here, when receiving carbons of a MUC-PM for
        # example
        sender_nick = (tab and tab.own_nick
mathieui's avatar
mathieui committed
833
                       or self.core.own_nick) if sent else with_nick
834
        if ignore and not sent:
835
            self.core.events.trigger('ignored_private', message, tab)
836
837
            msg = config.get_by_tabname('private_auto_response', room_from)
            if msg and body:
mathieui's avatar
mathieui committed
838
839
                self.core.xmpp.send_message(
                    mto=jid.full, mbody=msg, mtype='chat')
840
            return
841
        self.core.events.trigger('private_msg', message, tab)
mathieui's avatar
mathieui committed
842
        body = xhtml.get_body_from_message_stanza(
mathieui's avatar
mathieui committed
843
            message, use_xhtml=use_xhtml, extract_images_to=tmp_dir)
844
845
846
        if not body or not tab:
            return
        replaced = False
847
        user = tab.parent_muc.get_user_by_name(with_nick)
mathieui's avatar
mathieui committed
848
        if message.xml.find('{urn:xmpp:message-correct:0}replace') is not None:
849
            replaced_id = message['replace']['id']
850
            if replaced_id != '' and config.get_by_tabname(
mathieui's avatar
mathieui committed
851
                    'group_corrections', room_from):
852
                try:
mathieui's avatar
mathieui committed
853
854
855
856
857
858
                    tab.modify_message(
                        body,
                        replaced_id,
                        message['id'],
                        user=user,
                        jid=message['from'],
859
                        nickname=sender_nick)
860
861
862
                    replaced = True
                except CorrectionError:
                    log.debug('Unable to correct a message', exc_info=True)
863
        if not replaced:
mathieui's avatar
mathieui committed
864
865
866
            tab.add_message(
                body,
                time=None,
867
868
                nickname=sender_nick,
                nick_color=get_theme().COLOR_OWN_NICK if sent else None,
mathieui's avatar
mathieui committed
869
870
871
872
                forced_user=user,
                identifier=message['id'],
                jid=message['from'],
                typ=1)
873
        if sent:
Maxime Buquet's avatar
Maxime Buquet committed
874
            tab.set_last_sent_message(message, correct=replaced)
875
876
        else:
            tab.last_remote_message = datetime.now()
877

878
        if not sent and 'private' in config.get('beep_on').split():
879
880
            if not config.get_by_tabname('disable_beep', jid.full):
                curses.beep()
881
        if tab is self.core.tabs.current_tab:
882
            self.core.refresh_window()
883
        else:
884
            tab.state = 'normal' if sent else 'private'
885
            self.core.refresh_tab_win()
mathieui's avatar
mathieui committed
886

887
    ### Chatstates ###
mathieui's avatar
mathieui committed
888

889
    def on_chatstate_active(self, message):
890
        self._on_chatstate(message, "active")
mathieui's avatar
mathieui committed
891

892
    def on_chatstate_inactive(self, message):
893
        self._on_chatstate(message, "inactive")
mathieui's avatar
mathieui committed
894

895
    def on_chatstate_composing(self, message):
896
        self._on_chatstate(message, "composing")
mathieui's avatar
mathieui committed
897

898
    def on_chatstate_paused(self, message):
899
        self._on_chatstate(message, "paused")
mathieui's avatar
mathieui committed
900

901
    def on_chatstate_gone(self, message):
902
        self._on_chatstate(message, "gone")
mathieui's avatar
mathieui committed
903

904
    def _on_chatstate(self, message, state):
905
        if message['type'] == 'chat':
906
            if not self._on_chatstate_normal_conversation(message, state):
907
908
                tab = self.core.tabs.by_name_and_class(message['from'].full,
                                                       tabs.PrivateTab)
909
910
                if not tab:
                    return
911
                self._on_chatstate_private_conversation(message, state)
912
913
914
        elif message['type'] == 'groupchat':
            self.on_chatstate_groupchat_conversation(message, state)

915
916
    def _on_chatstate_normal_conversation(self, message, state):
        tab = self.core.get_conversation_by_jid(message['from'], False)
917
918
        if not tab:
            return False
919
        self.core.events.trigger('normal_chatstate', message, tab)
Link Mauve's avatar