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: JID, nick: str, password: str = 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.password = password
67
        # buffered presences
68
        self.presence_buffer = []
69
        # userlist
70
        self.users = []  # type: List[User]
71
        # private conversations
72
        self.privates = []  # type: List[Tab]
73
        self.topic = ''
74
        self.topic_from = ''
75 76 77
        # Self ping event, so we can cancel it when we leave the room
        self.self_ping_event = None
        # UI stuff
78 79 80 81 82 83 84
        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()
85
        # List of ignored users
86
        self.ignores = []  # type: List[User]
87
        # keys
88 89
        self.register_keys()
        self.update_keys()
90
        # commands
91
        self.register_commands()
92
        self.update_commands()
93
        self.resize()
94 95 96

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

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

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

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

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

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

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

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

164 165
            if config.get_by_tabname('display_user_color_in_join_part',
                                     self.general_jid):
166
                color = dump_tuple(theme.COLOR_OWN_NICK)
167
            else:
168
                color = "3"
169

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

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

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

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

216 217 218 219 220
        if not self.joined:
            return

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

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

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

252 253 254
        valid_roles = ('none', 'visitor', 'participant', 'moderator')

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

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

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

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

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

        self._text_buffer.add_message(
mathieui's avatar
mathieui committed
319 320
            "\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
321

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

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

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

400
    def get_nick(self) -> str:
401 402 403 404 405 406 407
        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
408

mathieui's avatar
mathieui committed
409 410
    def get_text_window(self):
        return self.text_win
411

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

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

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

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

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

513
    def own_join(self, from_nick: str, new_user: User, status_codes: Set[str]):
514
        """
mathieui's avatar
mathieui committed
515
        Handle the last presence we received, entering the room
516
        """
mathieui's avatar
mathieui committed
517 518 519 520 521 522
        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'
523
        elif self != self.core.tabs.current_tab:
mathieui's avatar
mathieui committed
524
            self._state = 'joined'
525
        if (self.core.tabs.current_tab is self
mathieui's avatar
mathieui committed
526 527
                and self.core.status.show not in ('xa', 'away')):
            self.send_chat_state('active')
528 529
        theme = get_theme()
        new_user.color = theme.COLOR_OWN_NICK
530

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

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

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

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

mathieui's avatar
mathieui committed
641 642
    def on_user_join(self, from_nick, affiliation, show, status, role, jid,
                     color):
643
        """
mathieui's avatar
mathieui committed
644
        When a new user joins the groupchat
645
        """
mathieui's avatar
mathieui committed
646 647 648 649
        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
650 651 652 653 654 655 656 657 658
        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
659 660 661 662
            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
663 664 665
            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
666 667 668 669 670 671
                           'nick': from_nick,
                           'spec': char_join,
                           'color': color,
                           'info_col': info_col,
                           'color_spec': spec_col,
                       }
mathieui's avatar
mathieui committed
672 673 674 675
            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
676 677 678 679 680
                           'spec': char_join,
                           'nick': from_nick,
                           'color': color,
                           'jid': jid.full,
                           'info_col': info_col,
681
                           'jid_color': dump_tuple(theme.COLOR_MUC_JID),
mathieui's avatar
mathieui committed
682 683
                           'color_spec': spec_col,
                       }
mathieui's avatar
mathieui committed
684 685 686 687
            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
688 689
        new_nick = presence.xml.find(
            '{%s}x/{%s}item' % (NS_MUC_USER, NS_MUC_USER)).attrib['nick']
690
        old_color = user.color
mathieui's avatar
mathieui committed
691 692 693 694
        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)
695
            user.change_nick(new_nick)
696
        else:
697
            user.change_nick(new_nick)
mathieui's avatar
mathieui committed
698 699
            deterministic = config.get_by_tabname('deterministic_nick_colors',
                                                  self.name)
700 701
            color = config.get_by_tabname(new_nick, 'muc_colors') or None
            if color or deterministic:
mathieui's avatar
mathieui committed
702 703 704
                user.change_color(color, deterministic)
        self.users.remove(user)
        bisect.insort_left(self.users, user)
705

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

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

740 741 742
        theme = get_theme()
        info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT)
        char_kick = theme.CHAR_KICK
743

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

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

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

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

916 917 918 919
            error_leave_txt = ''
            if server_initiated:
                error_leave_txt = ' due to an error'

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

mathieui's avatar
mathieui committed
951 952
    def on_user_change_status(self, user, from_nick, from_room, affiliation,
                              role, show, status):
mathieui's avatar
mathieui committed
953
        """
Maxime Buquet's avatar
Maxime Buquet committed
954
        When a user changes her status
mathieui's avatar
mathieui committed
955 956
        """
        # build the message
mathieui's avatar
mathieui committed
957 958
        display_message = False  # flag to know if something significant enough
        # to be displayed has changed
mathieui's avatar
mathieui committed
959 960 961 962 963
        if config.get_by_tabname('display_user_color_in_join_part',
                                 self.general_jid):
            color = dump_tuple(user.color)
        else:
            color = 3
964
        info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
mathieui's avatar
mathieui committed
965 966
        if from_nick == self.own_nick:
            msg = '\x19%(color)s}You\x19%(info_col)s} changed: ' % {
967
                'info_col': info_col,
mathieui's avatar
mathieui committed
968 969
                'color': color
            }
mathieui's avatar
mathieui committed
970 971
        else:
            msg = '\x19%(color)s}%(nick)s\x19%(info_col)s} changed: ' % {
mathieui's avatar
mathieui committed
972 973
                'nick': from_nick,
                'color': color,
974
                'info_col': info_col
mathieui's avatar
mathieui committed
975
            }
mathieui's avatar
mathieui committed
976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994
        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]
                display_message = True
        if not display_message:
            return
mathieui's avatar
mathieui committed
995
        msg = msg[:-2]  # remove the last ", "
mathieui's avatar
mathieui committed
996 997 998 999
        hide_status_change = config.get_by_tabname('hide_status_change',
                                                   self.general_jid)
        if hide_status_change < -1:
            hide_status_change = -1
1000 1001 1002 1003 1004 1005
        if ((