muctab.py 78.5 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 bisect
11
import curses
12
import logging
13 14
import os
import random
15
import re
16
import functools
mathieui's avatar
mathieui committed
17
from datetime import datetime
18
from typing import Dict, Callable, List, Optional, Union, Set
19

20
from slixmpp import JID
21
from poezio.tabs import ChatTab, Tab, SHOW_NAME
22

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

39 40
log = logging.getLogger(__name__)

41
NS_MUC_USER = 'http://jabber.org/protocol/muc#user'
42
STATUS_XPATH = '{%s}x/{%s}status' % (NS_MUC_USER, NS_MUC_USER)
43

44 45
COMPARE_USERS_LAST_TALKED = lambda x: x.last_talked

46 47 48 49

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

58
    def __init__(self, core, jid, nick, password=None):
59
        ChatTab.__init__(self, core, jid)
60 61 62
        self.joined = False
        self._state = 'disconnected'
        # our nick in the MUC
63
        self.own_nick = nick
64
        # self User object
65
        self.own_user = None  # type: Optional[User]
66
        self.name = jid
67
        self.password = password
68
        # buffered presences
69
        self.presence_buffer = []
70
        # userlist
71
        self.users = []  # type: List[User]
72
        # private conversations
73
        self.privates = []  # type: List[Tab]
74
        self.topic = ''
75
        self.topic_from = ''
76 77 78
        # Self ping event, so we can cancel it when we leave the room
        self.self_ping_event = None
        # UI stuff
79 80 81 82 83 84 85
        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()
86
        # List of ignored users
87
        self.ignores = []  # type: List[User]
88
        # keys
89 90
        self.register_keys()
        self.update_keys()
91
        # commands
92
        self.register_commands()
93
        self.update_commands()
94
        self.resize()
95 96 97

    @property
    def general_jid(self):
98
        return self.name
99

100
    def check_send_chat_state(self) -> bool:
mathieui's avatar
mathieui committed
101 102 103
        "If we should send a chat state"
        return self.joined

104
    @property
105
    def last_connection(self) -> Optional[datetime]:
106 107 108 109 110
        last_message = self._text_buffer.last_message
        if last_message:
            return last_message.time
        return None

111
    @staticmethod
112
    @refresh_wrapper.always
113 114 115 116 117 118 119
    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
120
    @refresh_wrapper.always
121 122 123 124 125 126
    def remove_information_element(plugin_name: str) -> None:
        """
        Lets a plugin add its own information to the MucInfoWin
        """
        del MucTab.additional_information[plugin_name]

127 128
    def cancel_config(self, form):
        """
Kim Alvefur's avatar
Kim Alvefur committed
129
        The user do not want to send their config, send an iq cancel
130
        """
131
        muc.cancel_config(self.core.xmpp, self.name)
132 133 134 135
        self.core.close_tab()

    def send_config(self, form):
        """
Kim Alvefur's avatar
Kim Alvefur committed
136
        The user sends their config to the server
137
        """
138
        muc.configure_room(self.core.xmpp, self.name, form)
139 140
        self.core.close_tab()

141 142 143 144 145 146 147 148 149
    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:
150
            seconds = None
mathieui's avatar
mathieui committed
151 152 153 154 155 156 157 158
        muc.join_groupchat(
            self.core,
            self.name,
            self.own_nick,
            self.password,
            status=status.message,
            show=status.show,
            seconds=seconds)
159

160
    def leave_room(self, message: str):
mathieui's avatar
mathieui committed
161
        if self.joined:
162 163 164 165
            theme = get_theme()
            info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT)
            char_quit = theme.CHAR_QUIT
            spec_col = dump_tuple(theme.COLOR_QUIT_CHAR)
166

167 168
            if config.get_by_tabname('display_user_color_in_join_part',
                                     self.general_jid):
169
                color = dump_tuple(theme.COLOR_OWN_NICK)
170
            else:
171
                color = "3"
172

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

196
            self.add_message(msg, typ=2)
197
            self.disconnect()
mathieui's avatar
mathieui committed
198 199
            muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick,
                                message)
200
            self.core.disable_private_tabs(self.name, reason=msg)
201
        else:
mathieui's avatar
mathieui committed
202 203
            muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick,
                                message)
mathieui's avatar
mathieui committed
204

205 206 207 208
    def change_affiliation(self,
                           nick_or_jid: Union[str, JID],
                           affiliation: str,
                           reason=''):
209 210 211
        """
        Change the affiliation of a nick or JID
        """
mathieui's avatar
mathieui committed
212

213 214
        def callback(iq):
            if iq['type'] == 'error':
mathieui's avatar
mathieui committed
215 216 217 218
                self.core.information(
                    "Could not set affiliation '%s' for '%s'." %
                    (affiliation, nick_or_jid), "Warning")

219 220 221 222 223
        if not self.joined:
            return

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

244
    def change_role(self, nick: str, role: str, reason=''):
245 246 247
        """
        Change the role of a nick
        """
mathieui's avatar
mathieui committed
248

249 250
        def callback(iq):
            if iq['type'] == 'error':
mathieui's avatar
mathieui committed
251 252 253
                self.core.information(
                    "Could not set role '%s' for '%s'." % (role, nick),
                    "Warning")
mathieui's avatar
mathieui committed
254

255 256 257
        valid_roles = ('none', 'visitor', 'participant', 'moderator')

        if not self.joined or role not in valid_roles:
mathieui's avatar
mathieui committed
258 259
            return self.core.information(
                'The role must be one of ' + ', '.join(valid_roles), 'Error')
260 261 262

        if not safeJID(self.name + '/' + nick):
            return self.core.information('Invalid nick', 'Info')
mathieui's avatar
mathieui committed
263 264
        muc.set_user_role(
            self.core.xmpp, self.name, nick, reason, role, callback=callback)
265

mathieui's avatar
mathieui committed
266
    @refresh_wrapper.conditional
267
    def print_info(self, nick: str) -> bool:
mathieui's avatar
mathieui committed
268 269 270 271 272 273 274 275 276
        """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
277
                inf, dump_tuple(theme.COLOR_MUC_JID), user.jid, inf)
mathieui's avatar
mathieui committed
278 279 280 281 282 283
        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
284 285 286 287 288 289 290 291 292 293 294
                    '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
295 296 297
        self.add_message(info, typ=0)
        return True

298
    def change_topic(self, topic: str):
mathieui's avatar
mathieui committed
299 300 301 302 303 304 305 306
        """Change the current topic"""
        muc.change_subject(self.core.xmpp, self.name, topic)

    @refresh_wrapper.always
    def show_topic(self):
        """
        Print the current topic
        """
307 308 309
        theme = get_theme()
        info_text = dump_tuple(theme.COLOR_INFORMATION_TEXT)
        norm_text = dump_tuple(theme.COLOR_NORMAL_TEXT)
mathieui's avatar
mathieui committed
310 311 312 313
        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
314 315
                user_string = '\x19%s}(set by \x19%s}%s\x19%s})' % (
                    info_text, user_text, user.nick, info_text)
mathieui's avatar
mathieui committed
316 317 318 319 320 321
            else:
                user_string = self.topic_from
        else:
            user_string = ''

        self._text_buffer.add_message(
mathieui's avatar
mathieui committed
322 323
            "\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
324

mathieui's avatar
mathieui committed
325 326 327
    @refresh_wrapper.always
    def recolor(self, random_colors=False):
        """Recolor the current MUC users"""
mathieui's avatar
mathieui committed
328 329
        deterministic = config.get_by_tabname('deterministic_nick_colors',
                                              self.name)
mathieui's avatar
mathieui committed
330 331 332 333 334 335 336 337 338 339 340
        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
341
        sorted_users = sorted(self.users, key=COMPARE_USERS_LAST_TALKED, reverse=True)
mathieui's avatar
mathieui committed
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
        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)

359
    @refresh_wrapper.conditional
360
    def set_nick_color(self, nick: str, color: str) -> bool:
361 362 363 364 365 366 367 368
        "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
369 370
                self.core.information('Color for nick %s unset' % (nick),
                                      'Info')
371 372 373 374 375 376
        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
377 378
            nick_color_aliases = config.get_by_tabname('nick_color_aliases',
                                                       self.name)
379 380 381 382 383 384 385 386 387 388 389
            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
390

mathieui's avatar
mathieui committed
391 392 393 394 395 396
    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
397 398 399
        empty_after = empty_after or (
            self.input.get_text().startswith('/')
            and not self.input.get_text().startswith('//'))
mathieui's avatar
mathieui committed
400 401
        self.send_composing_chat_state(empty_after)
        return False
402

403
    def get_nick(self) -> str:
404 405 406 407 408 409 410
        if config.get('show_muc_jid'):
            return self.name
        bookmark = self.core.bookmarks[self.name]
        if bookmark is not None and bookmark.name:
            return bookmark.name
        # TODO: send the disco#info identity name here, if it exists.
        return safeJID(self.name).user
411

mathieui's avatar
mathieui committed
412 413
    def get_text_window(self):
        return self.text_win
414

mathieui's avatar
mathieui committed
415 416 417 418
    def on_lose_focus(self):
        if self.joined:
            if self.input.text:
                self.state = 'nonempty'
419 420
            elif self.lagged:
                self.state = 'disconnected'
mathieui's avatar
mathieui committed
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436
            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
437 438
        if self.joined and config.get_by_tabname(
                'send_chat_states',
mathieui's avatar
mathieui committed
439 440 441 442
                self.general_jid) and not self.input.get_text():
            self.send_chat_state('active')

    def handle_presence(self, presence):
443
        """
mathieui's avatar
mathieui committed
444
        Handle MUC presence
445
        """
446
        self.reset_lag()
mathieui's avatar
mathieui committed
447 448 449
        status_codes = set()
        for status_code in presence.xml.findall(STATUS_XPATH):
            status_codes.add(status_code.attrib['code'])
mathieui's avatar
mathieui committed
450 451 452
        if presence['type'] == 'error':
            self.core.room_error(presence, self.name)
        elif not self.joined:
453 454 455
            own = '110' in status_codes or self.own_nick == presence['from'].resource
            if own or len(self.presence_buffer) >= 10:
                self.process_presence_buffer(presence, own)
mathieui's avatar
mathieui committed
456 457 458 459 460 461 462 463
            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)
464
        if self.core.tabs.current_tab is self:
mathieui's avatar
mathieui committed
465 466
            self.text_win.refresh()
            self.user_win.refresh_if_changed(self.users)
467 468 469
            self.info_header.refresh(
                self, self.text_win, user=self.own_user,
                information=MucTab.additional_information)
mathieui's avatar
mathieui committed
470 471
            self.input.refresh()
            self.core.doupdate()
472

473
    def process_presence_buffer(self, last_presence, own):
474
        """
mathieui's avatar
mathieui committed
475
        Batch-process all the initial presences
476
        """
mathieui's avatar
mathieui committed
477 478
        deterministic = config.get_by_tabname('deterministic_nick_colors',
                                              self.name)
mathieui's avatar
mathieui committed
479

mathieui's avatar
mathieui committed
480 481 482 483 484
        for stanza in self.presence_buffer:
            try:
                self.handle_presence_unjoined(stanza, deterministic)
            except PresenceError:
                self.core.room_error(stanza, stanza['from'].bare)
485 486
        self.presence_buffer = []
        self.handle_presence_unjoined(last_presence, deterministic, own)
mathieui's avatar
mathieui committed
487 488 489
        self.users.sort()
        # Enable the self ping event, to regularly check if we
        # are still in the room.
490 491
        if own:
            self.enable_self_ping_event()
492
        if self.core.tabs.current_tab is not self:
mathieui's avatar
mathieui committed
493
            self.refresh_tab_win()
494
            self.core.tabs.current_tab.refresh_input()
mathieui's avatar
mathieui committed
495
            self.core.doupdate()
496

mathieui's avatar
mathieui committed
497 498 499 500
    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
501
        from_nick, _, affiliation, show, status, role, jid, typ = dissect_presence(
mathieui's avatar
mathieui committed
502
            presence)
503 504
        if typ == 'unavailable':
            return
mathieui's avatar
mathieui committed
505
        user_color = self.search_for_color(from_nick)
mathieui's avatar
mathieui committed
506 507
        new_user = User(from_nick, affiliation, show, status, role, jid,
                        deterministic, user_color)
mathieui's avatar
mathieui committed
508 509 510 511 512 513 514
        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)
515

516
    def own_join(self, from_nick: str, new_user: User, status_codes: Set[str]):
517
        """
mathieui's avatar
mathieui committed
518
        Handle the last presence we received, entering the room
519
        """
mathieui's avatar
mathieui committed
520 521 522 523 524 525
        self.own_nick = from_nick
        self.own_user = new_user
        self.joined = True
        if self.name in self.core.initial_joins:
            self.core.initial_joins.remove(self.name)
            self._state = 'normal'
526
        elif self != self.core.tabs.current_tab:
mathieui's avatar
mathieui committed
527
            self._state = 'joined'
528
        if (self.core.tabs.current_tab is self
mathieui's avatar
mathieui committed
529 530
                and self.core.status.show not in ('xa', 'away')):
            self.send_chat_state('active')
531 532
        theme = get_theme()
        new_user.color = theme.COLOR_OWN_NICK
533

mathieui's avatar
mathieui committed
534 535 536 537
        if config.get_by_tabname('display_user_color_in_join_part',
                                 self.general_jid):
            color = dump_tuple(new_user.color)
        else:
538
            color = "3"
539

540 541 542
        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
543 544 545 546
        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,
547
                              'spec': theme.CHAR_JOIN,
mathieui's avatar
mathieui committed
548 549 550 551
                              'color_spec': spec_col,
                              'nick_col': color,
                              'info_col': info_col,
                          }
mathieui's avatar
mathieui committed
552 553 554 555
        self.add_message(enable_message, typ=2)
        self.core.enable_private_tabs(self.name, enable_message)
        if '201' in status_codes:
            self.add_message(
mathieui's avatar
mathieui committed
556 557
                '\x19%(info_col)s}Info: The room '
                'has been created' % {'info_col': info_col},
mathieui's avatar
mathieui committed
558 559 560
                typ=0)
        if '170' in status_codes:
            self.add_message(
mathieui's avatar
mathieui committed
561
                '\x19%(warn_col)s}Warning:\x19%(info_col)s}'
mathieui's avatar
mathieui committed
562 563 564 565
                ' This room is publicly logged' % {
                    'info_col': info_col,
                    'warn_col': warn_col
                },
mathieui's avatar
mathieui committed
566 567 568
                typ=0)
        if '100' in status_codes:
            self.add_message(
mathieui's avatar
mathieui committed
569
                '\x19%(warn_col)s}Warning:\x19%(info_col)s}'
mathieui's avatar
mathieui committed
570 571 572 573
                ' This room is not anonymous.' % {
                    'info_col': info_col,
                    'warn_col': warn_col
                },
mathieui's avatar
mathieui committed
574
                typ=0)
575

mathieui's avatar
mathieui committed
576
    def handle_presence_joined(self, presence, status_codes):
577
        """
mathieui's avatar
mathieui committed
578
        Handle new presences when we are already in the room
579
        """
mathieui's avatar
mathieui committed
580 581
        from_nick, from_room, affiliation, show, status, role, jid, typ = dissect_presence(
            presence)
mathieui's avatar
mathieui committed
582 583 584 585
        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'
586
        server_initiated = '333' in status_codes and typ == 'unavailable'
mathieui's avatar
mathieui committed
587 588 589 590 591 592
        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
593 594
            self.on_user_join(from_nick, affiliation, show, status, role, jid,
                              user_color)
mathieui's avatar
mathieui committed
595 596 597 598 599 600 601 602
        elif user is None:
            log.error('BUG: User %s in %s is None', from_nick, self.name)
            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
603 604
            self.core.on_user_left_private_conversation(
                from_room, user, status)
mathieui's avatar
mathieui committed
605
            self.on_user_banned(presence, user, from_nick)
606
        elif kick and not server_initiated:
mathieui's avatar
mathieui committed
607
            self.core.events.trigger('muc_kick', presence, self)
mathieui's avatar
mathieui committed
608 609
            self.core.on_user_left_private_conversation(
                from_room, user, status)
mathieui's avatar
mathieui committed
610 611 612 613 614 615 616 617 618
            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
619
            self.on_user_leave_groupchat(user, jid, status, from_nick,
620
                                         from_room, server_initiated)
mathieui's avatar
mathieui committed
621
        # status change
622
        else:
mathieui's avatar
mathieui committed
623 624
            self.on_user_change_status(user, from_nick, from_room, affiliation,
                                       role, show, status)
625

mathieui's avatar
mathieui committed
626 627 628
    def on_non_member_kicked(self):
        """We have been kicked because the MUC is members-only"""
        self.add_message(
mathieui's avatar
mathieui committed
629 630 631
            '\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
632 633 634 635 636 637
            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
638 639 640
            '\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
641 642 643
            typ=2)
        self.disconnect()

mathieui's avatar
mathieui committed
644 645
    def on_user_join(self, from_nick, affiliation, show, status, role, jid,
                     color):
646
        """
mathieui's avatar
mathieui committed
647
        When a new user joins the groupchat
648
        """
mathieui's avatar
mathieui committed
649 650 651 652
        deterministic = config.get_by_tabname('deterministic_nick_colors',
                                              self.name)
        user = User(from_nick, affiliation, show, status, role, jid,
                    deterministic, color)
mathieui's avatar
mathieui committed
653 654 655 656 657 658 659 660 661
        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
662 663 664 665
            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
666 667 668
            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
669 670 671 672 673 674
                           'nick': from_nick,
                           'spec': char_join,
                           'color': color,
                           'info_col': info_col,
                           'color_spec': spec_col,
                       }
mathieui's avatar
mathieui committed
675 676 677 678
            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
679 680 681 682 683
                           'spec': char_join,
                           'nick': from_nick,
                           'color': color,
                           'jid': jid.full,
                           'info_col': info_col,
684
                           'jid_color': dump_tuple(theme.COLOR_MUC_JID),
mathieui's avatar
mathieui committed
685 686
                           'color_spec': spec_col,
                       }
mathieui's avatar
mathieui committed
687 688 689 690
            self.add_message(msg, typ=2)
        self.core.on_user_rejoined_private_conversation(self.name, from_nick)

    def on_user_nick_change(self, presence, user, from_nick, from_room):
mathieui's avatar
mathieui committed
691 692
        new_nick = presence.xml.find(
            '{%s}x/{%s}item' % (NS_MUC_USER, NS_MUC_USER)).attrib['nick']
693
        old_color = user.color
mathieui's avatar
mathieui committed
694 695 696 697
        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)
698
            user.change_nick(new_nick)
699
        else:
700
            user.change_nick(new_nick)
mathieui's avatar
mathieui committed
701 702
            deterministic = config.get_by_tabname('deterministic_nick_colors',
                                                  self.name)
703 704
            color = config.get_by_tabname(new_nick, 'muc_colors') or None
            if color or deterministic:
mathieui's avatar
mathieui committed
705 706 707
                user.change_color(color, deterministic)
        self.users.remove(user)
        bisect.insort_left(self.users, user)
708

mathieui's avatar
mathieui committed
709 710 711
        if config.get_by_tabname('display_user_color_in_join_part',
                                 self.general_jid):
            color = dump_tuple(user.color)
712
            old_color = dump_tuple(old_color)
713
        else:
714
            old_color = color = 3
mathieui's avatar
mathieui committed
715
        info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
mathieui's avatar
mathieui committed
716
        self.add_message(
717
            '\x19%(old_color)s}%(old)s\x19%(info_col)s} is'
mathieui's avatar
mathieui committed
718 719 720 721
            ' now known as \x19%(color)s}%(new)s' % {
                'old': from_nick,
                'new': new_nick,
                'color': color,
722
                'old_color': old_color,
mathieui's avatar
mathieui committed
723 724 725
                'info_col': info_col
            },
            typ=2)
mathieui's avatar
mathieui committed
726 727
        # rename the private tabs if needed
        self.core.rename_private_tabs(self.name, from_nick, user)
728

mathieui's avatar
mathieui committed
729
    def on_user_banned(self, presence, user, from_nick):
730
        """
mathieui's avatar
mathieui committed
731
        When someone is banned from a muc
732
        """
mathieui's avatar
mathieui committed
733 734 735 736 737 738 739 740 741
        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
742

743 744 745
        theme = get_theme()
        info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT)
        char_kick = theme.CHAR_KICK
746

mathieui's avatar
mathieui committed
747
        if from_nick == self.own_nick:  # we are banned
mathieui's avatar
mathieui committed
748 749 750
            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
751 752 753 754
                                'spec': char_kick,
                                'by': by,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
755 756 757
            else:
                kick_msg = ('\x191}%(spec)s \x193}You\x19'
                            '%(info_col)s} have been banned.') % {
mathieui's avatar
mathieui committed
758 759 760
                                'spec': char_kick,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
761 762 763
            self.core.disable_private_tabs(self.name, reason=kick_msg)
            self.disconnect()
            self.refresh_tab_win()
764
            self.core.tabs.current_tab.refresh_input()
mathieui's avatar
mathieui committed
765 766 767 768 769 770 771
            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:
                    muc.join_groupchat(self.core, self.name, self.own_nick)
                else:
mathieui's avatar
mathieui committed
772 773 774
                    self.core.add_timed_event(
                        timed_events.DelayedEvent(delay, muc.join_groupchat,
                                                  self.core, self.name,
mathieui's avatar
mathieui committed
775
                                                  self.own_nick))
776

mathieui's avatar
mathieui committed
777 778 779 780 781 782 783 784 785 786 787
        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
788 789 790 791 792 793
                                'spec': char_kick,
                                'nick': from_nick,
                                'color': color,
                                'by': by,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
794 795 796
            else:
                kick_msg = ('\x191}%(spec)s \x19%(color)s}%(nick)s'
                            '\x19%(info_col)s} has been banned') % {
mathieui's avatar
mathieui committed
797 798 799 800 801
                                'spec': char_kick,
                                'nick': from_nick,
                                'color': color,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
802 803 804
        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
805 806 807
                             'reason': reason.text,
                             'info_col': info_col
                         }
mathieui's avatar
mathieui committed
808 809 810
        self.add_message(kick_msg, typ=2)

    def on_user_kicked(self, presence, user, from_nick):
811
        """
mathieui's avatar
mathieui committed
812
        When someone is kicked from a muc
813
        """
mathieui's avatar
mathieui committed
814 815 816 817 818 819
        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
820 821 822
        theme = get_theme()
        info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT)
        char_kick = theme.CHAR_KICK
mathieui's avatar
mathieui committed
823 824
        if actor_elem is not None:
            by = actor_elem.get('nick') or actor_elem.get('jid')
mathieui's avatar
mathieui committed
825
        if from_nick == self.own_nick:  # we are kicked
mathieui's avatar
mathieui committed
826 827 828 829
            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
830 831 832 833
                                'spec': char_kick,
                                'by': by,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
834 835 836
            else:
                kick_msg = ('\x191}%(spec)s \x193}You\x19%(info_col)s}'
                            ' have been kicked.') % {
mathieui's avatar
mathieui committed
837 838 839
                                'spec': char_kick,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
840 841 842
            self.core.disable_private_tabs(self.name, reason=kick_msg)
            self.disconnect()
            self.refresh_tab_win()
843
            self.core.tabs.current_tab.refresh_input()
mathieui's avatar
mathieui committed
844 845 846 847 848 849 850 851
            # 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:
                    muc.join_groupchat(self.core, self.name, self.own_nick)
                else:
mathieui's avatar
mathieui committed
852 853 854 855
                    self.core.add_timed_event(
                        timed_events.DelayedEvent(delay, muc.join_groupchat,
                                                  self.core, self.name,
                                                  self.own_nick))
mathieui's avatar
mathieui committed
856 857 858 859 860 861 862 863 864 865
        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
866 867 868 869 870 871
                                'spec': char_kick,
                                'nick': from_nick,
                                'color': color,
                                'by': by,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
872 873 874
            else:
                kick_msg = ('\x191}%(spec)s \x19%(color)s}%(nick)s'
                            '\x19%(info_col)s} has been kicked') % {
mathieui's avatar
mathieui committed
875 876 877 878 879
                                'spec': char_kick,
                                'nick': from_nick,
                                'color': color,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
880 881 882
        if reason is not None and reason.text:
            kick_msg += ('\x19%(info_col)s} Reason: \x196}'
                         '%(reason)s') % {
mathieui's avatar
mathieui committed
883 884 885
                             'reason': reason.text,
                             'info_col': info_col
                         }
mathieui's avatar
mathieui committed
886 887
        self.add_message(kick_msg, typ=2)

mathieui's avatar
mathieui committed
888
    def on_user_leave_groupchat(self,
889 890 891 892 893
                                user: User,
                                jid: JID,
                                status: str,
                                from_nick: str,
                                from_room: JID,
894
                                server_initiated=False):
mathieui's avatar
mathieui committed
895
        """
Maxime Buquet's avatar
Maxime Buquet committed
896
        When a user leaves a groupchat
mathieui's avatar
mathieui committed
897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914
        """
        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
915 916 917
            theme = get_theme()
            info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT)
            spec_col = dump_tuple(theme.COLOR_QUIT_CHAR)
mathieui's avatar
mathieui committed
918

919 920 921 922
            error_leave_txt = ''
            if server_initiated:
                error_leave_txt = ' due to an error'

mathieui's avatar
mathieui committed
923 924 925
            if not jid.full:
                leave_msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}'
                             '%(nick)s\x19%(info_col)s} has left the '
926
                             'room%(error_leave)s') % {
mathieui's avatar
mathieui committed
927 928
                                 'nick': from_nick,
                                 'color': color,
929
                                 'spec': theme.CHAR_QUIT,
mathieui's avatar
mathieui committed
930
                                 'info_col': info_col,
931 932
                                 'color_spec': spec_col,
                                 'error_leave': error_leave_txt,
mathieui's avatar
mathieui committed
933
                             }
mathieui's avatar
mathieui committed
934
            else:
935
                jid_col = dump_tuple(theme.COLOR_MUC_JID)
mathieui's avatar
mathieui committed
936 937 938
                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 '
939
                             'room%(error_leave)s') % {
940
                                 'spec': theme.CHAR_QUIT,
mathieui's avatar
mathieui committed
941 942 943 944 945
                                 'nick': from_nick,
                                 'color': color,
                                 'jid': jid.full,
                                 'info_col': info_col,
                                 'color_spec': spec_col,
946 947
                                 'jid_col': jid_col,
                                 'error_leave': error_leave_txt,
mathieui's avatar
mathieui committed
948
                             }
mathieui's avatar
mathieui committed
949 950 951
            if status:
                leave_msg += ' (\x19o%s\x19%s})' % (status, info_col)
            self.add_message(leave_msg, typ=2)
mathieui's avatar
mathieui committed
952
        self.core.on_user_left_private_conversation(from_room, user, status)
mathieui's avatar
mathieui committed
953

mathieui's avatar
mathieui committed
954 955
    def on_user_change_status(self, user, from_nick, from_room, affiliation,
                              role, show, status):
mathieui's avatar
mathieui committed
956
        """
Maxime Buquet's avatar
Maxime Buquet committed
957
        When a user changes her status
mathieui's avatar
mathieui committed
958 959
        """
        # build the message
mathieui's avatar
mathieui committed
960 961
        display_message = False  # flag to know if something significant enough
        # to be displayed has changed
mathieui's avatar
mathieui committed
962 963 964 965 966
        if config.get_by_tabname('display_user_color_in_join_part',
                                 self.general_jid):
            color = dump_tuple(user.color)
        else:
            color = 3
967
        info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
mathieui's avatar
mathieui committed
968 969
        if from_nick == self.own_nick:
            msg = '\x19%(color)s}You\x19%(info_col)s} changed: ' % {
970
                'info_col': info_col,
mathieui's avatar
mathieui committed
971 972
                'color': color
            }
mathieui's avatar
mathieui committed
973 974
        else:
            msg = '\x19%(color)s}%(nick)s\x19%(info_col)s} changed: ' % {
mathieui's avatar
mathieui committed
975 976
                'nick': from_nick,
                'color': color,
977
                'info_col': info_col
mathieui's avatar
mathieui committed
978
            }
mathieui's avatar
mathieui committed
979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997
        if affiliation != user.affiliation:
            msg += 'affiliation: %s, ' % affiliation
            display_message = True
        if role != user.role:
            msg += 'role: %s, ' % role
            display_message = True
        if show != user.show and show in SHOW_NAME:
            msg += 'show: %s, ' % SHOW_NAME[show]
            display_message = True
        if status != user.status:
            # if the user sets his status to nothing
            if status:
                msg += 'status: %s, ' % status
                display_message = True
            elif show in SHOW_NAME and show == user.show:
                msg += 'show: %s, ' % SHOW_NAME[show]