muctab.py 80.3 KB
Newer Older
1
2
3
4
5
6
7
8
9
"""
Module for the MucTab

A MucTab is a tab for multi-user chats as defined in XEP-0045.

It keeps track of many things such as part/joins, maintains an
user list, and updates private tabs when necessary.
"""

10
import asyncio
11
import bisect
12
import curses
13
import logging
Madhur Garg's avatar
Madhur Garg committed
14
import asyncio
15
16
import os
import random
17
import re
18
import functools
19
from copy import copy
mathieui's avatar
mathieui committed
20
from datetime import datetime
21
from typing import Dict, Callable, List, Optional, Union, Set
22

23
from slixmpp import InvalidJID, JID
24
from slixmpp.exceptions import IqError, IqTimeout
25
from poezio.tabs import ChatTab, Tab, SHOW_NAME
26

27
28
from poezio import common
from poezio import fixes
29
from poezio import mam
30
31
32
33
34
35
from poezio import multiuserchat as muc
from poezio import timed_events
from poezio import windows
from poezio import xhtml
from poezio.common import safeJID
from poezio.config import config
36
from poezio.core.structs import Command
37
38
39
40
41
from poezio.decorators import refresh_wrapper, command_args_parser
from poezio.logger import logger
from poezio.roster import roster
from poezio.theming import get_theme, dump_tuple
from poezio.user import User
42
from poezio.core.structs import Completion, Status
43

44
45
log = logging.getLogger(__name__)

46
NS_MUC_USER = 'http://jabber.org/protocol/muc#user'
47
STATUS_XPATH = '{%s}x/{%s}status' % (NS_MUC_USER, NS_MUC_USER)
48

49
50
COMPARE_USERS_LAST_TALKED = lambda x: x.last_talked

51
52
53
54

class MucTab(ChatTab):
    """
    The tab containing a multi-user-chat room.
Maxime Buquet's avatar
Maxime Buquet committed
55
    It contains a userlist, an input, a topic, an information and a chat zone
56
57
    """
    message_type = 'groupchat'
58
59
    plugin_commands = {}  # type: Dict[str, Command]
    plugin_keys = {}  # type: Dict[str, Callable]
60
    additional_information = {}  # type: Dict[str, Callable[[str], str]]
61
    lagged = False
62

63
    def __init__(self, core, jid, nick, password=None):
64
        ChatTab.__init__(self, core, jid)
65
66
67
        self.joined = False
        self._state = 'disconnected'
        # our nick in the MUC
68
        self.own_nick = nick
69
        # self User object
70
        self.own_user = None  # type: Optional[User]
71
        self.password = password
72
        # buffered presences
73
        self.presence_buffer = []
74
        # userlist
75
        self.users = []  # type: List[User]
76
        # private conversations
77
        self.privates = []  # type: List[Tab]
78
        self.topic = ''
79
        self.topic_from = ''
80
81
82
        # Self ping event, so we can cancel it when we leave the room
        self.self_ping_event = None
        # UI stuff
83
84
85
86
87
88
89
        self.topic_win = windows.Topic()
        self.text_win = windows.TextWin()
        self._text_buffer.add_window(self.text_win)
        self.v_separator = windows.VerticalSeparator()
        self.user_win = windows.UserList()
        self.info_header = windows.MucInfoWin()
        self.input = windows.MessageInput()
90
        # List of ignored users
91
        self.ignores = []  # type: List[User]
92
        # keys
93
94
        self.register_keys()
        self.update_keys()
95
        # commands
96
        self.register_commands()
97
        self.update_commands()
98
        self.resize()
99
100
101

    @property
    def general_jid(self):
102
        return self.jid
103

104
    def check_send_chat_state(self) -> bool:
mathieui's avatar
mathieui committed
105
106
107
        "If we should send a chat state"
        return self.joined

108
    @property
109
    def last_connection(self) -> Optional[datetime]:
110
111
112
113
114
        last_message = self._text_buffer.last_message
        if last_message:
            return last_message.time
        return None

115
    @staticmethod
116
    @refresh_wrapper.always
117
118
119
120
121
122
123
    def add_information_element(plugin_name: str, callback: Callable[[str], str]) -> None:
        """
        Lets a plugin add its own information to the MucInfoWin
        """
        MucTab.additional_information[plugin_name] = callback

    @staticmethod
124
    @refresh_wrapper.always
125
126
127
128
129
130
    def remove_information_element(plugin_name: str) -> None:
        """
        Lets a plugin add its own information to the MucInfoWin
        """
        del MucTab.additional_information[plugin_name]

131
132
    def cancel_config(self, form):
        """
Kim Alvefur's avatar
Kim Alvefur committed
133
        The user do not want to send their config, send an iq cancel
134
        """
135
        muc.cancel_config(self.core.xmpp, self.jid.bare)
136
137
138
139
        self.core.close_tab()

    def send_config(self, form):
        """
Kim Alvefur's avatar
Kim Alvefur committed
140
        The user sends their config to the server
141
        """
142
        muc.configure_room(self.core.xmpp, self.jid.bare, form)
143
144
        self.core.close_tab()

145
146
147
148
149
150
151
152
153
    def join(self):
        """
        Join the room
        """
        status = self.core.get_status()
        if self.last_connection:
            delta = datetime.now() - self.last_connection
            seconds = delta.seconds + delta.days * 24 * 3600
        else:
154
            seconds = None
mathieui's avatar
mathieui committed
155
156
        muc.join_groupchat(
            self.core,
157
            self.jid.bare,
mathieui's avatar
mathieui committed
158
159
160
161
162
            self.own_nick,
            self.password,
            status=status.message,
            show=status.show,
            seconds=seconds)
Madhur Garg's avatar
Madhur Garg committed
163
        asyncio.ensure_future(mam.on_tab_open(self))
164

165
    def leave_room(self, message: str):
mathieui's avatar
mathieui committed
166
        if self.joined:
167
168
169
170
            theme = get_theme()
            info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT)
            char_quit = theme.CHAR_QUIT
            spec_col = dump_tuple(theme.COLOR_QUIT_CHAR)
171

172
173
            if config.get_by_tabname('display_user_color_in_join_part',
                                     self.general_jid):
174
                color = dump_tuple(theme.COLOR_OWN_NICK)
175
            else:
176
                color = "3"
177

mathieui's avatar
mathieui committed
178
            if message:
179
180
                msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
                       'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
181
                       ' left the room'
182
                       ' (\x19o%(reason)s\x19%(info_col)s})') % {
mathieui's avatar
mathieui committed
183
184
185
186
                           'info_col': info_col,
                           'reason': message,
                           'spec': char_quit,
                           'color': color,
187
188
189
                           'color_spec': spec_col,
                           'nick': self.own_nick,
                       }
190
            else:
191
192
                msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
                       'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
193
                       ' left the room') % {
194
                           'info_col': info_col,
mathieui's avatar
mathieui committed
195
196
                           'spec': char_quit,
                           'color': color,
197
198
199
                           'color_spec': spec_col,
                           'nick': self.own_nick,
                       }
200

201
            self.add_message(msg, typ=2)
202
            self.disconnect()
203
            muc.leave_groupchat(self.core.xmpp, self.jid.bare, self.own_nick,
mathieui's avatar
mathieui committed
204
                                message)
205
            self.core.disable_private_tabs(self.jid.bare, reason=msg)
206
        else:
207
            muc.leave_groupchat(self.core.xmpp, self.jid.bare, self.own_nick,
mathieui's avatar
mathieui committed
208
                                message)
mathieui's avatar
mathieui committed
209

210
211
212
213
    def change_affiliation(self,
                           nick_or_jid: Union[str, JID],
                           affiliation: str,
                           reason=''):
214
215
216
        """
        Change the affiliation of a nick or JID
        """
mathieui's avatar
mathieui committed
217

218
219
        def callback(iq):
            if iq['type'] == 'error':
mathieui's avatar
mathieui committed
220
221
222
223
                self.core.information(
                    "Could not set affiliation '%s' for '%s'." %
                    (affiliation, nick_or_jid), "Warning")

224
225
226
227
228
        if not self.joined:
            return

        valid_affiliations = ('outcast', 'none', 'member', 'admin', 'owner')
        if affiliation not in valid_affiliations:
mathieui's avatar
mathieui committed
229
230
231
            return self.core.information(
                'The affiliation must be one of ' +
                ', '.join(valid_affiliations), 'Error')
232
        if nick_or_jid in [user.nick for user in self.users]:
mathieui's avatar
mathieui committed
233
            muc.set_user_affiliation(
mathieui's avatar
mathieui committed
234
                self.core.xmpp,
235
                self.jid.bare,
mathieui's avatar
mathieui committed
236
237
238
239
                affiliation,
                nick=nick_or_jid,
                callback=callback,
                reason=reason)
240
        else:
mathieui's avatar
mathieui committed
241
            muc.set_user_affiliation(
mathieui's avatar
mathieui committed
242
                self.core.xmpp,
243
                self.jid.bare,
mathieui's avatar
mathieui committed
244
245
246
247
                affiliation,
                jid=safeJID(nick_or_jid),
                callback=callback,
                reason=reason)
248

249
    def change_role(self, nick: str, role: str, reason=''):
250
251
252
        """
        Change the role of a nick
        """
mathieui's avatar
mathieui committed
253

254
255
        def callback(iq):
            if iq['type'] == 'error':
mathieui's avatar
mathieui committed
256
257
258
                self.core.information(
                    "Could not set role '%s' for '%s'." % (role, nick),
                    "Warning")
mathieui's avatar
mathieui committed
259

260
261
262
        valid_roles = ('none', 'visitor', 'participant', 'moderator')

        if not self.joined or role not in valid_roles:
mathieui's avatar
mathieui committed
263
264
            return self.core.information(
                'The role must be one of ' + ', '.join(valid_roles), 'Error')
265

266
267
268
269
        try:
            target_jid = copy(self.jid)
            target_jid.resource = nick
        except InvalidJID:
270
            return self.core.information('Invalid nick', 'Info')
271

mathieui's avatar
mathieui committed
272
        muc.set_user_role(
273
            self.core.xmpp, self.jid.bare, nick, reason, role, callback=callback)
274

mathieui's avatar
mathieui committed
275
    @refresh_wrapper.conditional
276
    def print_info(self, nick: str) -> bool:
mathieui's avatar
mathieui committed
277
278
279
280
281
282
283
284
285
        """Print information about a user"""
        user = self.get_user_by_name(nick)
        if not user:
            return False

        theme = get_theme()
        inf = '\x19' + dump_tuple(theme.COLOR_INFORMATION_TEXT) + '}'
        if user.jid:
            user_jid = '%s (\x19%s}%s\x19o%s)' % (
mathieui's avatar
mathieui committed
286
                inf, dump_tuple(theme.COLOR_MUC_JID), user.jid, inf)
mathieui's avatar
mathieui committed
287
288
289
290
291
292
        else:
            user_jid = ''
        info = ('\x19%(user_col)s}%(nick)s\x19o%(jid)s%(info)s: show: '
                '\x19%(show_col)s}%(show)s\x19o%(info)s, affiliation: '
                '\x19%(role_col)s}%(affiliation)s\x19o%(info)s, role: '
                '\x19%(role_col)s}%(role)s\x19o%(status)s') % {
mathieui's avatar
mathieui committed
293
294
295
296
297
298
299
300
301
302
303
                    'user_col': dump_tuple(user.color),
                    'nick': nick,
                    'jid': user_jid,
                    'info': inf,
                    'show_col': dump_tuple(theme.color_show(user.show)),
                    'show': user.show or 'Available',
                    'role_col': dump_tuple(theme.color_role(user.role)),
                    'affiliation': user.affiliation or 'None',
                    'role': user.role or 'None',
                    'status': '\n%s' % user.status if user.status else ''
                }
mathieui's avatar
mathieui committed
304
305
306
        self.add_message(info, typ=0)
        return True

307
    def change_topic(self, topic: str):
mathieui's avatar
mathieui committed
308
        """Change the current topic"""
309
        muc.change_subject(self.core.xmpp, self.jid.bare, topic)
mathieui's avatar
mathieui committed
310
311
312
313
314
315

    @refresh_wrapper.always
    def show_topic(self):
        """
        Print the current topic
        """
316
317
318
        theme = get_theme()
        info_text = dump_tuple(theme.COLOR_INFORMATION_TEXT)
        norm_text = dump_tuple(theme.COLOR_NORMAL_TEXT)
mathieui's avatar
mathieui committed
319
320
321
322
        if self.topic_from:
            user = self.get_user_by_name(self.topic_from)
            if user:
                user_text = dump_tuple(user.color)
mathieui's avatar
mathieui committed
323
324
                user_string = '\x19%s}(set by \x19%s}%s\x19%s})' % (
                    info_text, user_text, user.nick, info_text)
mathieui's avatar
mathieui committed
325
326
327
328
329
330
            else:
                user_string = self.topic_from
        else:
            user_string = ''

        self._text_buffer.add_message(
mathieui's avatar
mathieui committed
331
332
            "\x19%s}The subject of the room is: \x19%s}%s %s" %
            (info_text, norm_text, self.topic, user_string))
mathieui's avatar
mathieui committed
333

mathieui's avatar
mathieui committed
334
335
336
    @refresh_wrapper.always
    def recolor(self, random_colors=False):
        """Recolor the current MUC users"""
mathieui's avatar
mathieui committed
337
        deterministic = config.get_by_tabname('deterministic_nick_colors',
338
                                              self.jid.bare)
mathieui's avatar
mathieui committed
339
340
341
342
343
344
345
346
347
348
349
        if deterministic:
            for user in self.users:
                if user is self.own_user:
                    continue
                color = self.search_for_color(user.nick)
                if color != '':
                    continue
                user.set_deterministic_color()
            return
        # Sort the user list by last talked, to avoid color conflicts
        # on active participants
350
        sorted_users = sorted(self.users, key=COMPARE_USERS_LAST_TALKED, reverse=True)
mathieui's avatar
mathieui committed
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
        full_sorted_users = sorted_users[:]
        # search our own user, to remove it from the list
        # Also remove users whose color is fixed
        for user in full_sorted_users:
            color = self.search_for_color(user.nick)
            if user is self.own_user:
                sorted_users.remove(user)
            elif color != '':
                sorted_users.remove(user)
                user.change_color(color, deterministic)
        colors = list(get_theme().LIST_COLOR_NICKNAMES)
        if random_colors:
            random.shuffle(colors)
        for i, user in enumerate(sorted_users):
            user.color = colors[i % len(colors)]
        self.text_win.rebuild_everything(self._text_buffer)

368
    @refresh_wrapper.conditional
369
    def set_nick_color(self, nick: str, color: str) -> bool:
370
371
372
373
374
375
376
377
        "Set a custom color for a nick, permanently"
        user = self.get_user_by_name(nick)
        if color not in xhtml.colors and color not in ('unset', 'random'):
            return False
        if nick == self.own_nick:
            return False
        if color == 'unset':
            if config.remove_and_save(nick, 'muc_colors'):
mathieui's avatar
mathieui committed
378
379
                self.core.information('Color for nick %s unset' % (nick),
                                      'Info')
380
381
382
383
384
385
        else:
            if color == 'random':
                color = random.choice(list(xhtml.colors))
            if user:
                user.change_color(color)
            config.set_and_save(nick, color, 'muc_colors')
mathieui's avatar
mathieui committed
386
            nick_color_aliases = config.get_by_tabname('nick_color_aliases',
387
                                                       self.jid.bare)
388
389
390
391
392
393
394
395
396
397
398
            if nick_color_aliases:
                # if any user in the room has a nick which is an alias of the
                # nick, update its color
                for tab in self.core.get_tabs(MucTab):
                    for u in tab.users:
                        nick_alias = re.sub('^_*', '', u.nick)
                        nick_alias = re.sub('_*$', '', nick_alias)
                        if nick_alias == nick:
                            u.change_color(color)
        self.text_win.rebuild_everything(self._text_buffer)
        return True
mathieui's avatar
mathieui committed
399

mathieui's avatar
mathieui committed
400
401
402
403
404
405
    def on_input(self, key, raw):
        if not raw and key in self.key_func:
            self.key_func[key]()
            return False
        self.input.do_command(key, raw=raw)
        empty_after = self.input.get_text() == ''
mathieui's avatar
mathieui committed
406
407
408
        empty_after = empty_after or (
            self.input.get_text().startswith('/')
            and not self.input.get_text().startswith('//'))
mathieui's avatar
mathieui committed
409
410
        self.send_composing_chat_state(empty_after)
        return False
411

412
    def get_nick(self) -> str:
413
        if config.get('show_muc_jid'):
414
415
            return self.jid.bare
        bookmark = self.core.bookmarks[self.jid.bare]
416
417
418
        if bookmark is not None and bookmark.name:
            return bookmark.name
        # TODO: send the disco#info identity name here, if it exists.
419
        return self.jid.user
420

mathieui's avatar
mathieui committed
421
422
    def get_text_window(self):
        return self.text_win
423

mathieui's avatar
mathieui committed
424
425
426
427
    def on_lose_focus(self):
        if self.joined:
            if self.input.text:
                self.state = 'nonempty'
428
429
            elif self.lagged:
                self.state = 'disconnected'
mathieui's avatar
mathieui committed
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
            else:
                self.state = 'normal'
        else:
            self.state = 'disconnected'
        self.text_win.remove_line_separator()
        self.text_win.add_line_separator(self._text_buffer)
        if config.get_by_tabname('send_chat_states', self.general_jid):
            self.send_chat_state('inactive')
        self.check_scrolled()

    def on_gain_focus(self):
        self.state = 'current'
        if (self.text_win.built_lines and self.text_win.built_lines[-1] is None
                and not config.get('show_useless_separator')):
            self.text_win.remove_line_separator()
        curses.curs_set(1)
mathieui's avatar
mathieui committed
446
447
        if self.joined and config.get_by_tabname(
                'send_chat_states',
mathieui's avatar
mathieui committed
448
449
450
451
                self.general_jid) and not self.input.get_text():
            self.send_chat_state('active')

    def handle_presence(self, presence):
452
        """
mathieui's avatar
mathieui committed
453
        Handle MUC presence
454
        """
455
        self.reset_lag()
mathieui's avatar
mathieui committed
456
457
458
        status_codes = set()
        for status_code in presence.xml.findall(STATUS_XPATH):
            status_codes.add(status_code.attrib['code'])
mathieui's avatar
mathieui committed
459
        if presence['type'] == 'error':
460
            self.core.room_error(presence, self.jid.bare)
mathieui's avatar
mathieui committed
461
        elif not self.joined:
462
            own = '110' in status_codes
463
464
            if own or len(self.presence_buffer) >= 10:
                self.process_presence_buffer(presence, own)
mathieui's avatar
mathieui committed
465
466
467
468
469
470
471
472
            else:
                self.presence_buffer.append(presence)
                return
        else:
            try:
                self.handle_presence_joined(presence, status_codes)
            except PresenceError:
                self.core.room_error(presence, presence['from'].bare)
473
        if self.core.tabs.current_tab is self:
mathieui's avatar
mathieui committed
474
475
            self.text_win.refresh()
            self.user_win.refresh_if_changed(self.users)
476
477
478
            self.info_header.refresh(
                self, self.text_win, user=self.own_user,
                information=MucTab.additional_information)
mathieui's avatar
mathieui committed
479
480
            self.input.refresh()
            self.core.doupdate()
481

482
    def process_presence_buffer(self, last_presence, own):
483
        """
mathieui's avatar
mathieui committed
484
        Batch-process all the initial presences
485
        """
mathieui's avatar
mathieui committed
486
        deterministic = config.get_by_tabname('deterministic_nick_colors',
487
                                              self.jid.bare)
mathieui's avatar
mathieui committed
488

mathieui's avatar
mathieui committed
489
490
491
492
493
        for stanza in self.presence_buffer:
            try:
                self.handle_presence_unjoined(stanza, deterministic)
            except PresenceError:
                self.core.room_error(stanza, stanza['from'].bare)
494
495
        self.presence_buffer = []
        self.handle_presence_unjoined(last_presence, deterministic, own)
mathieui's avatar
mathieui committed
496
497
498
        self.users.sort()
        # Enable the self ping event, to regularly check if we
        # are still in the room.
499
500
        if own:
            self.enable_self_ping_event()
501
        if self.core.tabs.current_tab is not self:
mathieui's avatar
mathieui committed
502
            self.refresh_tab_win()
503
            self.core.tabs.current_tab.refresh_input()
mathieui's avatar
mathieui committed
504
            self.core.doupdate()
505

mathieui's avatar
mathieui committed
506
507
508
509
    def handle_presence_unjoined(self, presence, deterministic, own=False):
        """
        Presence received while we are not in the room (before code=110)
        """
mathieui's avatar
mathieui committed
510
        from_nick, _, affiliation, show, status, role, jid, typ = dissect_presence(
mathieui's avatar
mathieui committed
511
            presence)
512
513
        if typ == 'unavailable':
            return
mathieui's avatar
mathieui committed
514
        user_color = self.search_for_color(from_nick)
mathieui's avatar
mathieui committed
515
516
        new_user = User(from_nick, affiliation, show, status, role, jid,
                        deterministic, user_color)
mathieui's avatar
mathieui committed
517
518
519
520
521
522
523
        self.users.append(new_user)
        self.core.events.trigger('muc_join', presence, self)
        if own:
            status_codes = set()
            for status_code in presence.xml.findall(STATUS_XPATH):
                status_codes.add(status_code.attrib['code'])
            self.own_join(from_nick, new_user, status_codes)
524

525
    def own_join(self, from_nick: str, new_user: User, status_codes: Set[str]):
526
        """
mathieui's avatar
mathieui committed
527
        Handle the last presence we received, entering the room
528
        """
mathieui's avatar
mathieui committed
529
530
531
        self.own_nick = from_nick
        self.own_user = new_user
        self.joined = True
532
533
        if self.jid.bare in self.core.initial_joins:
            self.core.initial_joins.remove(self.jid.bare)
mathieui's avatar
mathieui committed
534
            self._state = 'normal'
535
        elif self != self.core.tabs.current_tab:
mathieui's avatar
mathieui committed
536
            self._state = 'joined'
537
        if (self.core.tabs.current_tab is self
mathieui's avatar
mathieui committed
538
539
                and self.core.status.show not in ('xa', 'away')):
            self.send_chat_state('active')
540
541
        theme = get_theme()
        new_user.color = theme.COLOR_OWN_NICK
542

mathieui's avatar
mathieui committed
543
544
545
546
        if config.get_by_tabname('display_user_color_in_join_part',
                                 self.general_jid):
            color = dump_tuple(new_user.color)
        else:
547
            color = "3"
548

549
550
551
        info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT)
        warn_col = dump_tuple(theme.COLOR_WARNING_TEXT)
        spec_col = dump_tuple(theme.COLOR_JOIN_CHAR)
mathieui's avatar
mathieui committed
552
553
554
555
        enable_message = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} You '
                          '(\x19%(nick_col)s}%(nick)s\x19%(info_col)s}) joined'
                          ' the room') % {
                              'nick': from_nick,
556
                              'spec': theme.CHAR_JOIN,
mathieui's avatar
mathieui committed
557
558
559
560
                              'color_spec': spec_col,
                              'nick_col': color,
                              'info_col': info_col,
                          }
mathieui's avatar
mathieui committed
561
        self.add_message(enable_message, typ=2)
562
        self.core.enable_private_tabs(self.jid.bare, enable_message)
mathieui's avatar
mathieui committed
563
564
        if '201' in status_codes:
            self.add_message(
mathieui's avatar
mathieui committed
565
566
                '\x19%(info_col)s}Info: The room '
                'has been created' % {'info_col': info_col},
mathieui's avatar
mathieui committed
567
568
569
                typ=0)
        if '170' in status_codes:
            self.add_message(
mathieui's avatar
mathieui committed
570
                '\x19%(warn_col)s}Warning:\x19%(info_col)s}'
mathieui's avatar
mathieui committed
571
572
573
574
                ' This room is publicly logged' % {
                    'info_col': info_col,
                    'warn_col': warn_col
                },
mathieui's avatar
mathieui committed
575
576
577
                typ=0)
        if '100' in status_codes:
            self.add_message(
mathieui's avatar
mathieui committed
578
                '\x19%(warn_col)s}Warning:\x19%(info_col)s}'
mathieui's avatar
mathieui committed
579
580
581
582
                ' This room is not anonymous.' % {
                    'info_col': info_col,
                    'warn_col': warn_col
                },
mathieui's avatar
mathieui committed
583
                typ=0)
584

mathieui's avatar
mathieui committed
585
    def handle_presence_joined(self, presence, status_codes):
586
        """
mathieui's avatar
mathieui committed
587
        Handle new presences when we are already in the room
588
        """
mathieui's avatar
mathieui committed
589
590
        from_nick, from_room, affiliation, show, status, role, jid, typ = dissect_presence(
            presence)
mathieui's avatar
mathieui committed
591
592
593
594
        change_nick = '303' in status_codes
        kick = '307' in status_codes and typ == 'unavailable'
        ban = '301' in status_codes and typ == 'unavailable'
        shutdown = '332' in status_codes and typ == 'unavailable'
595
        server_initiated = '333' in status_codes and typ == 'unavailable'
mathieui's avatar
mathieui committed
596
597
598
599
600
601
        non_member = '322' in status_codes and typ == 'unavailable'
        user = self.get_user_by_name(from_nick)
        # New user
        if not user and typ != "unavailable":
            user_color = self.search_for_color(from_nick)
            self.core.events.trigger('muc_join', presence, self)
mathieui's avatar
mathieui committed
602
603
            self.on_user_join(from_nick, affiliation, show, status, role, jid,
                              user_color)
mathieui's avatar
mathieui committed
604
        elif user is None:
605
            log.error('BUG: User %s in %s is None', from_nick, self.jid.bare)
mathieui's avatar
mathieui committed
606
607
608
609
610
611
            return
        elif change_nick:
            self.core.events.trigger('muc_nickchange', presence, self)
            self.on_user_nick_change(presence, user, from_nick, from_room)
        elif ban:
            self.core.events.trigger('muc_ban', presence, self)
mathieui's avatar
mathieui committed
612
613
            self.core.on_user_left_private_conversation(
                from_room, user, status)
mathieui's avatar
mathieui committed
614
            self.on_user_banned(presence, user, from_nick)
615
        elif kick and not server_initiated:
mathieui's avatar
mathieui committed
616
            self.core.events.trigger('muc_kick', presence, self)
mathieui's avatar
mathieui committed
617
618
            self.core.on_user_left_private_conversation(
                from_room, user, status)
mathieui's avatar
mathieui committed
619
620
621
622
623
624
625
626
627
            self.on_user_kicked(presence, user, from_nick)
        elif shutdown:
            self.core.events.trigger('muc_shutdown', presence, self)
            self.on_muc_shutdown()
        elif non_member:
            self.core.events.trigger('muc_shutdown', presence, self)
            self.on_non_member_kicked()
        # user quit
        elif typ == 'unavailable':
mathieui's avatar
mathieui committed
628
            self.on_user_leave_groupchat(user, jid, status, from_nick,
629
                                         from_room, server_initiated)
mathieui's avatar
mathieui committed
630
        # status change
631
        else:
mathieui's avatar
mathieui committed
632
633
            self.on_user_change_status(user, from_nick, from_room, affiliation,
                                       role, show, status)
634

mathieui's avatar
mathieui committed
635
636
637
    def on_non_member_kicked(self):
        """We have been kicked because the MUC is members-only"""
        self.add_message(
mathieui's avatar
mathieui committed
638
639
640
            '\x19%(info_col)s}You have been kicked because you '
            'are not a member and the room is now members-only.' %
            {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
mathieui's avatar
mathieui committed
641
642
643
644
645
646
            typ=2)
        self.disconnect()

    def on_muc_shutdown(self):
        """We have been kicked because the MUC service is shutting down"""
        self.add_message(
mathieui's avatar
mathieui committed
647
648
649
            '\x19%(info_col)s}You have been kicked because the'
            ' MUC service is shutting down.' %
            {'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)},
mathieui's avatar
mathieui committed
650
651
652
            typ=2)
        self.disconnect()

mathieui's avatar
mathieui committed
653
654
    def on_user_join(self, from_nick, affiliation, show, status, role, jid,
                     color):
655
        """
mathieui's avatar
mathieui committed
656
        When a new user joins the groupchat
657
        """
mathieui's avatar
mathieui committed
658
        deterministic = config.get_by_tabname('deterministic_nick_colors',
659
                                              self.jid.bare)
mathieui's avatar
mathieui committed
660
661
        user = User(from_nick, affiliation, show, status, role, jid,
                    deterministic, color)
mathieui's avatar
mathieui committed
662
663
664
665
666
667
668
669
670
        bisect.insort_left(self.users, user)
        hide_exit_join = config.get_by_tabname('hide_exit_join',
                                               self.general_jid)
        if hide_exit_join != 0:
            if config.get_by_tabname('display_user_color_in_join_part',
                                     self.general_jid):
                color = dump_tuple(user.color)
            else:
                color = 3
671
672
673
674
            theme = get_theme()
            info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT)
            spec_col = dump_tuple(theme.COLOR_JOIN_CHAR)
            char_join = theme.CHAR_JOIN
mathieui's avatar
mathieui committed
675
676
677
            if not jid.full:
                msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}%(nick)s'
                       '\x19%(info_col)s} joined the room') % {
mathieui's avatar
mathieui committed
678
679
680
681
682
683
                           'nick': from_nick,
                           'spec': char_join,
                           'color': color,
                           'info_col': info_col,
                           'color_spec': spec_col,
                       }
mathieui's avatar
mathieui committed
684
685
686
687
            else:
                msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}%(nick)s'
                       '\x19%(info_col)s} (\x19%(jid_color)s}%(jid)s\x19'
                       '%(info_col)s}) joined the room') % {
mathieui's avatar
mathieui committed
688
689
690
691
692
                           'spec': char_join,
                           'nick': from_nick,
                           'color': color,
                           'jid': jid.full,
                           'info_col': info_col,
693
                           'jid_color': dump_tuple(theme.COLOR_MUC_JID),
mathieui's avatar
mathieui committed
694
695
                           'color_spec': spec_col,
                       }
mathieui's avatar
mathieui committed
696
            self.add_message(msg, typ=2)
697
        self.core.on_user_rejoined_private_conversation(self.jid.bare, from_nick)
mathieui's avatar
mathieui committed
698
699

    def on_user_nick_change(self, presence, user, from_nick, from_room):
mathieui's avatar
mathieui committed
700
701
        new_nick = presence.xml.find(
            '{%s}x/{%s}item' % (NS_MUC_USER, NS_MUC_USER)).attrib['nick']
702
        old_color = user.color
mathieui's avatar
mathieui committed
703
704
705
706
        if user.nick == self.own_nick:
            self.own_nick = new_nick
            # also change our nick in all private discussions of this room
            self.core.handler.on_muc_own_nickchange(self)
707
            user.change_nick(new_nick)
708
        else:
709
            user.change_nick(new_nick)
mathieui's avatar
mathieui committed
710
            deterministic = config.get_by_tabname('deterministic_nick_colors',
711
                                                  self.jid.bare)
712
713
            color = config.get_by_tabname(new_nick, 'muc_colors') or None
            if color or deterministic:
mathieui's avatar
mathieui committed
714
715
716
                user.change_color(color, deterministic)
        self.users.remove(user)
        bisect.insort_left(self.users, user)
717

mathieui's avatar
mathieui committed
718
719
720
        if config.get_by_tabname('display_user_color_in_join_part',
                                 self.general_jid):
            color = dump_tuple(user.color)
721
            old_color = dump_tuple(old_color)
722
        else:
723
            old_color = color = 3
mathieui's avatar
mathieui committed
724
        info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
mathieui's avatar
mathieui committed
725
        self.add_message(
726
            '\x19%(old_color)s}%(old)s\x19%(info_col)s} is'
mathieui's avatar
mathieui committed
727
728
729
730
            ' now known as \x19%(color)s}%(new)s' % {
                'old': from_nick,
                'new': new_nick,
                'color': color,
731
                'old_color': old_color,
mathieui's avatar
mathieui committed
732
733
734
                'info_col': info_col
            },
            typ=2)
mathieui's avatar
mathieui committed
735
        # rename the private tabs if needed
736
        self.core.rename_private_tabs(self.jid.bare, from_nick, user)
737

mathieui's avatar
mathieui committed
738
    def on_user_banned(self, presence, user, from_nick):
739
        """
mathieui's avatar
mathieui committed
740
        When someone is banned from a muc
741
        """
mathieui's avatar
mathieui committed
742
743
744
745
746
747
748
749
750
        self.users.remove(user)
        by = presence.xml.find('{%s}x/{%s}item/{%s}actor' %
                               (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER))
        reason = presence.xml.find('{%s}x/{%s}item/{%s}reason' %
                                   (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER))
        if by:
            by = by.get('jid') or by.get('nick') or None
        else:
            by = None
751

752
753
754
        theme = get_theme()
        info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT)
        char_kick = theme.CHAR_KICK
755

mathieui's avatar
mathieui committed
756
        if from_nick == self.own_nick:  # we are banned
mathieui's avatar
mathieui committed
757
758
759
            if by:
                kick_msg = ('\x191}%(spec)s \x193}You\x19%(info_col)s}'
                            ' have been banned by \x194}%(by)s') % {
mathieui's avatar
mathieui committed
760
761
762
763
                                'spec': char_kick,
                                'by': by,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
764
765
766
            else:
                kick_msg = ('\x191}%(spec)s \x193}You\x19'
                            '%(info_col)s} have been banned.') % {
mathieui's avatar
mathieui committed
767
768
769
                                'spec': char_kick,
                                'info_col': info_col
                            }
770
            self.core.disable_private_tabs(self.jid.bare, reason=kick_msg)
mathieui's avatar
mathieui committed
771
772
            self.disconnect()
            self.refresh_tab_win()
773
            self.core.tabs.current_tab.refresh_input()
mathieui's avatar
mathieui committed
774
775
776
777
778
            if config.get_by_tabname('autorejoin', self.general_jid):
                delay = config.get_by_tabname('autorejoin_delay',
                                              self.general_jid)
                delay = common.parse_str_to_secs(delay)
                if delay <= 0:
779
                    muc.join_groupchat(self.core, self.jid.bare, self.own_nick)
mathieui's avatar
mathieui committed
780
                else:
mathieui's avatar
mathieui committed
781
782
                    self.core.add_timed_event(
                        timed_events.DelayedEvent(delay, muc.join_groupchat,
783
                                                  self.core, self.jid.bare,
mathieui's avatar
mathieui committed
784
                                                  self.own_nick))
785

mathieui's avatar
mathieui committed
786
787
788
789
790
791
792
793
794
795
796
        else:
            if config.get_by_tabname('display_user_color_in_join_part',
                                     self.general_jid):
                color = dump_tuple(user.color)
            else:
                color = 3

            if by:
                kick_msg = ('\x191}%(spec)s \x19%(color)s}'
                            '%(nick)s\x19%(info_col)s} '
                            'has been banned by \x194}%(by)s') % {
mathieui's avatar
mathieui committed
797
798
799
800
801
802
                                'spec': char_kick,
                                'nick': from_nick,
                                'color': color,
                                'by': by,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
803
804
805
            else:
                kick_msg = ('\x191}%(spec)s \x19%(color)s}%(nick)s'
                            '\x19%(info_col)s} has been banned') % {
mathieui's avatar
mathieui committed
806
807
808
809
810
                                'spec': char_kick,
                                'nick': from_nick,
                                'color': color,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
811
812
813
        if reason is not None and reason.text:
            kick_msg += ('\x19%(info_col)s} Reason: \x196}'
                         '%(reason)s\x19%(info_col)s}') % {
mathieui's avatar
mathieui committed
814
815
816
                             'reason': reason.text,
                             'info_col': info_col
                         }
mathieui's avatar
mathieui committed
817
818
819
        self.add_message(kick_msg, typ=2)

    def on_user_kicked(self, presence, user, from_nick):
820
        """
mathieui's avatar
mathieui committed
821
        When someone is kicked from a muc
822
        """
mathieui's avatar
mathieui committed
823
824
825
826
827
828
        self.users.remove(user)
        actor_elem = presence.xml.find('{%s}x/{%s}item/{%s}actor' %
                                       (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER))
        reason = presence.xml.find('{%s}x/{%s}item/{%s}reason' %
                                   (NS_MUC_USER, NS_MUC_USER, NS_MUC_USER))
        by = None
829
830
831
        theme = get_theme()
        info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT)
        char_kick = theme.CHAR_KICK
mathieui's avatar
mathieui committed
832
833
        if actor_elem is not None:
            by = actor_elem.get('nick') or actor_elem.get('jid')
mathieui's avatar
mathieui committed
834
        if from_nick == self.own_nick:  # we are kicked
mathieui's avatar
mathieui committed
835
836
837
838
            if by:
                kick_msg = ('\x191}%(spec)s \x193}You\x19'
                            '%(info_col)s} have been kicked'
                            ' by \x193}%(by)s') % {
mathieui's avatar
mathieui committed
839
840
841
842
                                'spec': char_kick,
                                'by': by,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
843
844
845
            else:
                kick_msg = ('\x191}%(spec)s \x193}You\x19%(info_col)s}'
                            ' have been kicked.') % {
mathieui's avatar
mathieui committed
846
847
848
                                'spec': char_kick,
                                'info_col': info_col
                            }
849
            self.core.disable_private_tabs(self.jid.bare, reason=kick_msg)
mathieui's avatar
mathieui committed
850
851
            self.disconnect()
            self.refresh_tab_win()
852
            self.core.tabs.current_tab.refresh_input()
mathieui's avatar
mathieui committed
853
854
855
856
857
858
            # try to auto-rejoin
            if config.get_by_tabname('autorejoin', self.general_jid):
                delay = config.get_by_tabname('autorejoin_delay',
                                              self.general_jid)
                delay = common.parse_str_to_secs(delay)
                if delay <= 0:
859
                    muc.join_groupchat(self.core, self.jid.bare, self.own_nick)
mathieui's avatar
mathieui committed
860
                else:
mathieui's avatar
mathieui committed
861
862
                    self.core.add_timed_event(
                        timed_events.DelayedEvent(delay, muc.join_groupchat,
863
                                                  self.core, self.jid.bare,
mathieui's avatar
mathieui committed
864
                                                  self.own_nick))
mathieui's avatar
mathieui committed
865
866
867
868
869
870
871
872
873
874
        else:
            if config.get_by_tabname('display_user_color_in_join_part',
                                     self.general_jid):
                color = dump_tuple(user.color)
            else:
                color = 3
            if by:
                kick_msg = ('\x191}%(spec)s \x19%(color)s}%(nick)s'
                            '\x19%(info_col)s} has been kicked by '
                            '\x193}%(by)s') % {
mathieui's avatar
mathieui committed
875
876
877
878
879
880
                                'spec': char_kick,
                                'nick': from_nick,
                                'color': color,
                                'by': by,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
881
882
883
            else:
                kick_msg = ('\x191}%(spec)s \x19%(color)s}%(nick)s'
                            '\x19%(info_col)s} has been kicked') % {
mathieui's avatar
mathieui committed
884
885
886
887
888
                                'spec': char_kick,
                                'nick': from_nick,
                                'color': color,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
889
890
891
        if reason is not None and reason.text:
            kick_msg += ('\x19%(info_col)s} Reason: \x196}'
                         '%(reason)s') % {
mathieui's avatar
mathieui committed
892
893
894
                             'reason': reason.text,
                             'info_col': info_col
                         }
mathieui's avatar
mathieui committed
895
896
        self.add_message(kick_msg, typ=2)

mathieui's avatar
mathieui committed
897
    def on_user_leave_groupchat(self,
898
899
900
901
902
                                user: User,
                                jid: JID,
                                status: str,
                                from_nick: str,
                                from_room: JID,
903
                                server_initiated=False):
mathieui's avatar
mathieui committed
904
        """
Maxime Buquet's avatar
Maxime Buquet committed
905
        When a user leaves a groupchat
mathieui's avatar
mathieui committed
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
        """
        self.users.remove(user)
        if self.own_nick == user.nick:
            # We are now out of the room.
            # Happens with some buggy (? not sure) servers
            self.disconnect()
            self.core.disable_private_tabs(from_room)
            self.refresh_tab_win()

        hide_exit_join = config.get_by_tabname('hide_exit_join',
                                               self.general_jid)

        if hide_exit_join <= -1 or user.has_talked_since(hide_exit_join):
            if config.get_by_tabname('display_user_color_in_join_part',
                                     self.general_jid):
                color = dump_tuple(user.color)
            else:
                color = 3
924
925
926
            theme = get_theme()
            info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT)
            spec_col = dump_tuple(theme.COLOR_QUIT_CHAR)
mathieui's avatar
mathieui committed
927

928
929
930
931
            error_leave_txt = ''
            if server_initiated:
                error_leave_txt = ' due to an error'

mathieui's avatar
mathieui committed
932
933
934
            if not jid.full:
                leave_msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}'
                             '%(nick)s\x19%(info_col)s} has left the '
935
                             'room%(error_leave)s') % {
mathieui's avatar
mathieui committed
936
937
                                 'nick': from_nick,
                                 'color': color,
938
                                 'spec': theme.CHAR_QUIT,
mathieui's avatar
mathieui committed
939
                                 'info_col': info_col,
940
941
                                 'color_spec': spec_col,
                                 'error_leave': error_leave_txt,
mathieui's avatar
mathieui committed
942
                             }
mathieui's avatar
mathieui committed
943
            else:
944
                jid_col = dump_tuple(theme.COLOR_MUC_JID)
mathieui's avatar
mathieui committed
945
946
947
                leave_msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}'
                             '%(nick)s\x19%(info_col)s} (\x19%(jid_col)s}'
                             '%(jid)s\x19%(info_col)s}) has left the '
948
                             'room%(error_leave)s') % {
949
                                 'spec': theme.CHAR_QUIT,
mathieui's avatar
mathieui committed
950
951
952
953
954
                                 'nick': from_nick,
                                 'color': color,
                                 'jid': jid.full,
                                 'info_col': info_col,
                                 'color_spec': spec_col,
955
956
                                 'jid_col': jid_col,
                                 'error_leave': error_leave_txt,
mathieui's avatar
mathieui committed
957
                             }
mathieui's avatar
mathieui committed
958
959
960
            if status:
                leave_msg += ' (\x19o%s\x19%s})' % (status, info_col)
            self.add_message(leave_msg, typ=2)
mathieui's avatar
mathieui committed
961
        self.core.on_user_left_private_conversation(from_room, user, status)
mathieui's avatar
mathieui committed
962

mathieui's avatar
mathieui committed
963
964
    def on_user_change_status(self, user, from_nick, from_room, affiliation,
                              role, show, status):
mathieui's avatar
mathieui committed
965
        """
Maxime Buquet's avatar
Maxime Buquet committed
966
        When a user changes her status
mathieui's avatar
mathieui committed
967
968
        """
        # build the message