handlers.py 53.3 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 ssl
mathieui's avatar
mathieui committed
12
import sys
mathieui's avatar
mathieui committed
13
import time
14
from datetime import datetime
15
from hashlib import sha1, sha512
16
from os import path
mathieui's avatar
mathieui committed
17

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

import common
23
import fixes
mathieui's avatar
mathieui committed
24
25
26
import pep
import tabs
import windows
27
import xhtml
mathieui's avatar
mathieui committed
28
29
import multiuserchat as muc
from common import safeJID
30
from config import config, CACHE_DIR
mathieui's avatar
mathieui committed
31
32
33
from contact import Resource
from logger import logger
from roster import roster
34
from text_buffer import CorrectionError, AckError
mathieui's avatar
mathieui committed
35
36
37
38
from theming import dump_tuple, get_theme

from . commands import dumb_callback

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

49
50
51
52
53
54
55
56
57
58
59
60
61

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']
62
            rostertab = self.core.get_tab_by_name('Roster', tabs.RosterInfoTab)
63
64
65
66
            rostertab.check_blocking(features)
            rostertab.check_saslexternal(features)
            if (config.get('enable_carbons') and
                    'urn:xmpp:carbons:2' in features):
67
68
                self.core.xmpp.plugin['xep_0280'].enable()
            self.core.check_bookmark_storage(features)
69

70
71
        self.core.xmpp.plugin['xep_0030'].get_info(jid=self.core.xmpp.boundjid.domain,
                                                   callback=callback)
72
73
74
75
76
77
78
79
80

    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):
81
            recv['to'] = self.core.xmpp.boundjid.full
82
83
84
85
86
87
88
            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'):
89
            fixes.has_identity(self.core.xmpp, recv['from'].server,
90
91
92
                               identity='conference',
                               on_true=functools.partial(ignore_message, recv),
                               on_false=functools.partial(receive_message, recv))
mathieui's avatar
mathieui committed
93
            return
94
95
96
97
98
99
100
101
102
103
104
        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):
105
            sent['from'] = self.core.xmpp.boundjid.full
106
107
108
109
110
            self.on_normal_message(sent)

        sent = message['carbon_sent']
        if (sent['to'].bare not in roster or
                roster[sent['to'].bare].subscription == 'none'):
111
            fixes.has_identity(self.core.xmpp, sent['to'].server,
112
113
114
115
116
117
118
119
120
121
122
123
124
                               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']
125
        if jid.bare in self.core.pending_invites:
126
127
            return
        # there are 2 'x' tags in the messages, making message['x'] useless
128
        invite = StanzaBase(self.core.xmpp, xml=message.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite'))
129
130
131
132
133
134
135
136
        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
137
        self.core.information(msg, 'Info')
138
139
140
        if 'invite' in config.get('beep_on').split():
            curses.beep()
        logger.log_roster_change(inviter.full, 'invited you to %s' % jid.full)
141
        self.core.pending_invites[jid.bare] = inviter.full
142
143
144
145
146
147
148
149
150
151

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

155
156
157
158
159
160
161
162
163
164
165
166
167
        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

168
        self.core.information(msg, 'Info')
169
170
171
        if 'invite' in config.get('beep_on').split():
            curses.beep()

172
        self.core.pending_invites[room.bare] = inviter.full
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
        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)
        """
        if message.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite') != None:
            return
        if message['type'] == 'groupchat':
            return
        # Differentiate both type of messages, and call the appropriate handler.
        jid_from = message['from']
188
        for tab in self.core.get_tabs(tabs.MucTab):
189
190
            if tab.name == jid_from.bare:
                if message['type'] == 'chat':
191
192
193
                    self.on_groupchat_private_message(message)
                    return
        self.on_normal_message(message)
194
195
196
197
198
199

    def on_error_message(self, message):
        """
        When receiving any message with type="error"
        """
        jid_from = message['from']
200
        for tab in self.core.get_tabs(tabs.MucTab):
201
202
            if tab.name == jid_from.bare:
                if message['type'] == 'error':
203
                    self.core.room_error(message, jid_from.bare)
204
                else:
205
206
207
208
                    self.on_groupchat_private_message(message)
                return
        tab = self.core.get_conversation_by_jid(message['from'], create=False)
        error_msg = self.core.get_error_message(message, deprecated=True)
209
        if not tab:
210
211
            self.core.information(error_msg, 'Error')
            return
212
213
214
215
        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)
216
            self.core.refresh_window()
217
218
219
220
221
222
223
224
225
226


    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']:
227
            return self.core.information('%s says: %s' % (message['from'], message['body']), 'Headline')
228
229
230
231
232
233
234
235
236

        use_xhtml = config.get('enable_xhtml_im')
        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
237

238
239
        remote_nick = ''
        # normal message, we are the recipient
240
        if message['to'].bare == self.core.xmpp.boundjid.bare:
241
242
243
244
245
246
247
248
249
250
            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']
251
            if not remote_nick:
252
253
254
255
256
                remote_nick = conv_jid.user
                if not remote_nick:
                    remote_nick = conv_jid.full
            own = False
        # we wrote the message (happens with carbons)
257
        elif message['from'].bare == self.core.xmpp.boundjid.bare:
258
            conv_jid = message['to']
259
            jid = self.core.xmpp.boundjid
260
            color = get_theme().COLOR_OWN_NICK
261
            remote_nick = self.core.own_nick
262
263
            own = True
        # we are not part of that message, drop it
mathieui's avatar
mathieui committed
264
        else:
265
266
            return

267
        conversation = self.core.get_conversation_by_jid(conv_jid, create=True)
268
269
270
271
272
273
274
275
        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
        elif not own: # keep a fixed nick during the whole conversation
            remote_nick = conversation.nick

276
        self.core.events.trigger('conversation_msg', message, conversation)
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
        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():
            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()
313
        if self.core.current_tab() is not conversation:
314
315
            if not own:
                conversation.state = 'private'
316
                self.core.refresh_tab_win()
317
318
            else:
                conversation.set_state('normal')
319
                self.core.refresh_tab_win()
320
        else:
321
            self.core.refresh_window()
mathieui's avatar
mathieui committed
322

323
324
325
326
327
328
329
330
331
332
333
    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']
        if item.xml.find('{http://jabber.org/protocol/nick}nick'):
            contact.name = item['nick']['nick']
mathieui's avatar
mathieui committed
334
        else:
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
            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
        if item.xml.find('{urn:xmpp:gaming:0}gaming'):
            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
359
        else:
360
            contact.gaming = {}
mathieui's avatar
mathieui committed
361

362
363
        if contact.gaming:
            logger.log_roster_change(contact.bare_jid, 'is playing %s' % (common.format_gaming_string(contact.gaming)))
mathieui's avatar
mathieui committed
364

365
366
        if old_gaming != contact.gaming and config.get_by_tabname('display_gaming_notifications', contact.bare_jid):
            if contact.gaming:
367
                self.core.information('%s is playing %s' % (contact.bare_jid, common.format_gaming_string(contact.gaming)), 'Gaming')
368
            else:
369
                self.core.information(contact.bare_jid + ' stopped playing.', 'Gaming')
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391

    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
        if item.xml.find('{http://jabber.org/protocol/mood}mood'):
            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
392
        else:
393
394
395
396
397
398
399
            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:
400
                self.core.information('Mood from '+ contact.bare_jid + ': ' + contact.mood, 'Mood')
401
            else:
402
                self.core.information(contact.bare_jid + ' stopped having his/her mood.', 'Mood')
403
404
405
406
407
408
409
410

    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
411
            return
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
        roster.modified()
        item = message['pubsub_event']['items']['item']
        old_activity = contact.activity
        if item.xml.find('{http://jabber.org/protocol/activity}activity'):
            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
431
432
433
434
        else:
            contact.activity = ''

        if contact.activity:
435
436
437
438
            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:
439
                self.core.information('Activity from '+ contact.bare_jid + ': ' + contact.activity, 'Activity')
440
            else:
441
                self.core.information(contact.bare_jid + ' stopped doing his/her activity.', 'Activity')
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464

    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
        if item.xml.find('{http://jabber.org/protocol/tune}tune'):
            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
465
        else:
466
467
            contact.tune = {}

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

471
472
        if old_tune != contact.tune and config.get_by_tabname('display_tune_notifications', contact.bare_jid):
            if contact.tune:
473
                self.core.information(
474
475
476
                        'Tune from '+ message['from'].bare + ': ' + common.format_tune_string(contact.tune),
                        'Tune')
            else:
477
                self.core.information(contact.bare_jid + ' stopped listening to music.', 'Tune')
mathieui's avatar
mathieui committed
478

479
480
481
482
483
484
485
    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
486

487
        if message['type'] == 'error': # Check if it's an error
488
489
            self.core.room_error(message, room_from)
            return
mathieui's avatar
mathieui committed
490

491
        tab = self.core.get_tab_by_name(room_from, tabs.MucTab)
492
        if not tab:
493
494
            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='')
495
            return
mathieui's avatar
mathieui committed
496

497
498
499
500
        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
501

502
        self.core.events.trigger('muc_msg', message, tab)
503
504
505
506
507
508
509
510
        use_xhtml = config.get('enable_xhtml_im')
        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
511

512
513
514
515
516
517
518
519
520
521
522
        old_state = tab.state
        delayed, date = common.find_delayed_tag(message)
        replaced_id = message['replace']['id']
        replaced = False
        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):
523
                    self.core.events.trigger('highlight', message, tab)
524
525
526
527
                replaced = True
            except CorrectionError:
                log.debug('Unable to correct a message', exc_info=True)
        if not replaced and tab.add_message(body, date, nick_from, history=delayed, identifier=message['id'], jid=message['from'], typ=1):
528
            self.core.events.trigger('highlight', message, tab)
mathieui's avatar
mathieui committed
529

530
531
532
        if message['from'].resource == tab.own_nick:
            tab.last_sent_message = message

533
        if tab is self.core.current_tab():
534
535
536
            tab.text_win.refresh()
            tab.info_header.refresh(tab, tab.text_win)
            tab.input.refresh()
537
            self.core.doupdate()
538
        elif tab.state != old_state:
539
540
            self.core.refresh_tab_win()
            current = self.core.current_tab()
541
542
            if hasattr(current, 'input') and current.input:
                current.input.refresh()
543
            self.core.doupdate()
544
545
546

        if 'message' in config.get('beep_on').split():
            if (not config.get_by_tabname('disable_beep', room_from)
547
                    and self.core.own_nick != message['from'].resource):
548
549
550
551
                curses.beep()

    def on_muc_own_nickchange(self, muc):
        "We changed our nick in a MUC"
552
        for tab in self.core.get_tabs(tabs.PrivateTab):
553
554
555
556
557
558
559
560
561
562
            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:
563
564
            self.on_groupchat_message(message)
            return
565
566
567
568
569
570
571
572

        room_from = jid.bare
        use_xhtml = config.get('enable_xhtml_im')
        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)
573
        tab = self.core.get_tab_by_name(jid.full, tabs.PrivateTab) # get the tab with the private conversation
574
575
576
        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:
577
                tab = self.core.open_private_window(room_from, nick_from, False)
578
        if ignore:
579
            self.core.events.trigger('ignored_private', message, tab)
580
581
            msg = config.get_by_tabname('private_auto_response', room_from)
            if msg and body:
582
                self.core.xmpp.send_message(mto=jid.full, mbody=msg, mtype='chat')
583
            return
584
        self.core.events.trigger('private_msg', message, tab)
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
        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_id = message['replace']['id']
        replaced = False
        user = tab.parent_muc.get_user_by_name(nick_from)
        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)
        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
611
            else:
612
613
614
615
                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()
616
617
        if tab is self.core.current_tab():
            self.core.refresh_window()
618
619
        else:
            tab.state = 'private'
620
            self.core.refresh_tab_win()
mathieui's avatar
mathieui committed
621

622
    ### Chatstates ###
mathieui's avatar
mathieui committed
623

624
    def on_chatstate_active(self, message):
625
        self._on_chatstate(message, "active")
mathieui's avatar
mathieui committed
626

627
    def on_chatstate_inactive(self, message):
628
        self._on_chatstate(message, "inactive")
mathieui's avatar
mathieui committed
629

630
    def on_chatstate_composing(self, message):
631
        self._on_chatstate(message, "composing")
mathieui's avatar
mathieui committed
632

633
    def on_chatstate_paused(self, message):
634
        self._on_chatstate(message, "paused")
mathieui's avatar
mathieui committed
635

636
    def on_chatstate_gone(self, message):
637
        self._on_chatstate(message, "gone")
mathieui's avatar
mathieui committed
638

639
    def _on_chatstate(self, message, state):
640
        if message['type'] == 'chat':
641
642
            if not self._on_chatstate_normal_conversation(message, state):
                tab = self.core.get_tab_by_name(message['from'].full, tabs.PrivateTab)
643
644
                if not tab:
                    return
645
                self._on_chatstate_private_conversation(message, state)
646
647
648
        elif message['type'] == 'groupchat':
            self.on_chatstate_groupchat_conversation(message, state)

649
650
    def _on_chatstate_normal_conversation(self, message, state):
        tab = self.core.get_conversation_by_jid(message['from'], False)
651
652
653
        if not tab:
            return False
        tab.remote_wants_chatstates = True
654
        self.core.events.trigger('normal_chatstate', message, tab)
655
656
        tab.chatstate = state
        if state == 'gone' and isinstance(tab, tabs.DynamicConversationTab):
mathieui's avatar
mathieui committed
657
            tab.unlock()
658
        if tab == self.core.current_tab():
659
            tab.refresh_info_header()
660
            self.core.doupdate()
661
662
        else:
            _composing_tab_state(tab, state)
663
            self.core.refresh_tab_win()
664
        return True
mathieui's avatar
mathieui committed
665

666
    def _on_chatstate_private_conversation(self, message, state):
667
668
669
        """
        Chatstate received in a private conversation from a MUC
        """
670
        tab = self.core.get_tab_by_name(message['from'].full, tabs.PrivateTab)
671
672
673
        if not tab:
            return
        tab.remote_wants_chatstates = True
674
        self.core.events.trigger('private_chatstate', message, tab)
675
        tab.chatstate = state
676
        if tab == self.core.current_tab():
677
            tab.refresh_info_header()
678
            self.core.doupdate()
mathieui's avatar
mathieui committed
679
        else:
680
            _composing_tab_state(tab, state)
681
            self.core.refresh_tab_win()
682
683
684
685
686
687
688

    def on_chatstate_groupchat_conversation(self, message, state):
        """
        Chatstate received in a MUC
        """
        nick = message['mucnick']
        room_from = message.get_mucroom()
689
        tab = self.core.get_tab_by_name(room_from, tabs.MucTab)
690
        if tab and tab.get_user_by_name(nick):
691
            self.core.events.trigger('muc_chatstate', message, tab)
692
            tab.get_user_by_name(nick).chatstate = state
693
694
        if tab == self.core.current_tab():
            if not self.core.size.tab_degrade_x:
695
696
                tab.user_win.refresh(tab.users)
            tab.input.refresh()
697
            self.core.doupdate()
698
699
        else:
            _composing_tab_state(tab, state)
700
            self.core.refresh_tab_win()
mathieui's avatar
mathieui committed
701

702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
    ### 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()
720
721
        if isinstance(self.core.current_tab(), tabs.RosterInfoTab):
            self.core.refresh_window()
mathieui's avatar
mathieui committed
722

723
724
725
726
727
728
729
    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':
730
731
            self.core.xmpp.sendPresence(pto=jid, ptype='subscribed')
            self.core.xmpp.sendPresence(pto=jid)
louiz’'s avatar
louiz’ committed
732
        else:
733
734
735
736
            if not contact:
                contact = roster.get_and_set(jid)
            roster.update_contact_groups(contact)
            contact.pending_in = True
737
738
739
740
741
            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'
742
            roster.modified()
743
744
        if isinstance(self.core.current_tab(), tabs.RosterInfoTab):
            self.core.refresh_window()
mathieui's avatar
mathieui committed
745

746
747
748
749
750
    def on_subscription_authorized(self, presence):
        """subscribed received"""
        jid = presence['from'].bare
        contact = roster[jid]
        if contact.subscription not in ('both', 'from'):
751
            self.core.information('%s accepted your contact proposal' % jid, 'Roster')
752
753
754
755
756
        if contact.pending_out:
            contact.pending_out = False

        roster.modified()

757
758
        if isinstance(self.core.current_tab(), tabs.RosterInfoTab):
            self.core.refresh_window()
759
760
761
762
763
764

    def on_subscription_remove(self, presence):
        """unsubscribe received"""
        jid = presence['from'].bare
        contact = roster[jid]
        if not contact:
mathieui's avatar
mathieui committed
765
            return
766
        roster.modified()
767
768
769
770
        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()
771
772
773
774
775
776

    def on_subscription_removed(self, presence):
        """unsubscribed received"""
        jid = presence['from'].bare
        contact = roster[jid]
        if not contact:
mathieui's avatar
mathieui committed
777
            return
778
779
        roster.modified()
        if contact.pending_out:
780
            self.core.information('%s rejected your contact proposal' % jid, 'Roster')
781
            contact.pending_out = False
782
        else:
783
784
785
786
            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()
787
788
789
790
791
792
793
794

    ### Presence-related handlers ###

    def on_presence(self, presence):
        if presence.match('presence/muc') or presence.xml.find('{http://jabber.org/protocol/muc#user}x'):
            return
        jid = presence['from']
        contact = roster[jid.bare]
795
        tab = self.core.get_conversation_by_jid(jid, create=False)
796
797
798
799
800
801
802
803
804
        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
805
806
807
808
809
        self.core.events.trigger('normal_presence', presence, contact[jid.full])
        tab = self.core.get_conversation_by_jid(jid, create=False)
        if isinstance(self.core.current_tab(), tabs.RosterInfoTab):
            self.core.refresh_window()
        elif self.core.current_tab() == tab:
810
            tab.refresh()
811
            self.core.doupdate()
mathieui's avatar
mathieui committed
812

813
814
815
816
817
818
819
820
    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
821
        tab = self.core.get_tab_by_name(jid.full, tabs.ConversationTab)
822
823
824
825
826
827
828
829
830
831
832
        if tab:
            tab.remote_wants_chatstates = None

    def on_got_offline(self, presence):
        """
        A JID got offline
        """
        if presence.match('presence/muc') or presence.xml.find('{http://jabber.org/protocol/muc#user}x'):
            return
        jid = presence['from']
        if not logger.log_roster_change(jid.bare, 'got offline'):
833
            self.core.information('Unable to write in the log file', 'Error')
834
835
836
837
838
839
840
841
842
        # 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:
843
844
845
            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')
846
        roster.modified()
847
848
        if isinstance(self.core.current_tab(), tabs.RosterInfoTab):
            self.core.refresh_window()
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863

    def on_got_online(self, presence):
        """
        A JID got online
        """
        if presence.match('presence/muc') or presence.xml.find('{http://jabber.org/protocol/muc#user}x'):
            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'):
864
            self.core.information('Unable to write in the log file', 'Error')
865
866
867
868
869
        resource = Resource(jid.full, {
            'priority': presence.get_priority() or 0,
            'status': presence['status'],
            'show': presence['show'],
            })
870
        self.core.events.trigger('normal_presence', presence, resource)
871
        name = contact.name if contact.name else jid.bare
872
873
        self.core.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x194}online' % name)
        if time.time() - self.core.connection_time > 10:
874
875
            # We do not display messages if we recently logged in
            if presence['status']:
876
                self.core.information("\x193}%s \x195}is \x194}online\x195} (\x19o%s\x195})" % (name, presence['status']), "Roster")
877
            else:
878
879
880
881
                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()
882
883
884
885
886
887
888
889

    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
890
        tab = self.core.get_tab_by_name(from_room, tabs.MucTab)
891
        if tab:
892
            self.core.events.trigger('muc_presence', presence, tab)
893
894
895
896
897
898
899
900
901
            tab.handle_presence(presence)


    ### Connection-related handlers ###

    def on_failed_connection(self, error):
        """
        We cannot contact the remote server
        """
902
        self.core.information("Connection to remote server failed: %s" % (error,), 'Error')
903
904
905
906
907
908
909

    def on_disconnected(self, event):
        """
        When we are disconnected from remote server
        """
        roster.connected = 0
        # Stop the ping plugin. It would try to send stanza on regular basis
910
        self.core.xmpp.plugin['xep_0199'].disable_keepalive()
911
        roster.modified()
912
        for tab in self.core.get_tabs(tabs.MucTab):
913
            tab.disconnect()
914
915
916
917
918
        msg_typ = 'Error' if not self.core.legitimate_disconnect else 'Info'
        self.core.information("Disconnected from server.", msg_typ)
        if not self.core.legitimate_disconnect and config.get('auto_reconnect', True):
            self.core.information("Auto-reconnecting.", 'Info')
            self.core.xmpp.start()
919
920
921
922
923
924

    def on_stream_error(self, event):
        """
        When we receive a stream error
        """
        if event and event['text']:
925
            self.core.information('Stream error: %s' % event['text'], 'Error')
926
927
928
929
930

    def on_failed_all_auth(self, event):
        """
        Authentication failed
        """
931
932
933
        self.core.information("Authentication failed (bad credentials?).",
                              'Error')
        self.core.legitimate_disconnect = True
934
935
936
937
938

    def on_no_auth(self, event):
        """
        Authentication failed (no mech)
        """
939
940
941
        self.core.information("Authentication failed, no login method available.",
                              'Error')
        self.core.legitimate_disconnect = True
942
943
944
945
946

    def on_connected(self, event):
        """
        Remote host responded, but we are not yet authenticated
        """
947
        self.core.information("Connected to server.", 'Info')
948
949
950
951
952

    def on_connecting(self, event):
        """
        Just before we try to connect to the server
        """
953
        self.core.legitimate_disconnect = False
954
955
956
957
958

    def on_session_start(self, event):
        """
        Called when we are connected and authenticated
        """
959
960
961
962
963
964
        self.core.connection_time = time.time()
        if not self.core.plugins_autoloaded: # Do not reload plugins on reconnection
            self.core.autoload_plugins()
        self.core.information("Authentication success.", 'Info')
        self.core.information("Your JID is %s" % self.core.xmpp.boundjid.full, 'Info')
        if not self.core.xmpp.anon:
965
            # request the roster
966
967
            self.core.xmpp.get_roster()
            roster.update_contact_groups(self.core.xmpp.boundjid.bare)
968
969
            # send initial presence
            if config.get('send_initial_presence'):
970
971
972
973
                pres = self.core.xmpp.make_presence()
                pres['show'] = self.core.status.show
                pres['status'] = self.core.status.message
                self.core.events.trigger('send_normal_presence', pres)
974
                pres.send()
975
        self.core.bookmarks.get_local()
976
        # join all the available bookmarks. As of yet, this is just the local ones
977
        self.core.join_initial_rooms(self.core.bookmarks)
978
979

        if config.get('enable_user_nick'):
980
981
            self.core.xmpp.plugin['xep_0172'].publish_nick(nick=self.core.own_nick, callback=dumb_callback)
        asyncio.async(self.core.xmpp.plugin['xep_0115'].update_caps())
982
        # Start the ping's plugin regular event
983
        self.core.xmpp.set_keepalive_values()
984
985
986
987
988
989
990
991
992

    ### Other handlers ###

    def on_status_codes(self, message):
        """
        Handle groupchat messages with status codes.
        Those are received when a room configuration change occurs.
        """
        room_from = message['from']
993
        tab = self.core.get_tab_by_name(room_from, tabs.MucTab)
994
995
        status_codes = set([s.attrib['code'] for s in message.findall('{%s}x/{%s}status' % (tabs.NS_MUC_USER, tabs.NS_MUC_USER))])
        if '101' in status_codes:
996
            self.core.information('Your affiliation in the room %s changed' % room_from, 'Info')
997
998
999
1000
        elif tab and status_codes:
            show_unavailable = '102' in status_codes
            hide_unavailable = '103' in status_codes
            non_priv = '104' in status_codes
For faster browsing, not all history is shown. View entire blame