muctab.py 79 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
Madhur Garg's avatar
Madhur Garg committed
13
import asyncio
14 15
import os
import random
16
import re
17
import functools
18
from copy import copy
mathieui's avatar
mathieui committed
19
from datetime import datetime
20
from typing import Dict, Callable, List, Optional, Union, Set
21

22
from slixmpp import InvalidJID, JID
23
from poezio.tabs import ChatTab, Tab, SHOW_NAME
24

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

42 43
log = logging.getLogger(__name__)

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

47 48
COMPARE_USERS_LAST_TALKED = lambda x: x.last_talked

49 50 51 52

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

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

    @property
    def general_jid(self):
100
        return self.jid
101

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

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

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

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

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

143 144 145 146 147 148 149 150 151
    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:
152
            seconds = None
mathieui's avatar
mathieui committed
153 154
        muc.join_groupchat(
            self.core,
155
            self.jid.bare,
mathieui's avatar
mathieui committed
156 157 158 159 160
            self.own_nick,
            self.password,
            status=status.message,
            show=status.show,
            seconds=seconds)
Madhur Garg's avatar
Madhur Garg committed
161
        asyncio.ensure_future(mam.on_tab_open(self))
162

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

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

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

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

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

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

222 223 224 225 226
        if not self.joined:
            return

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

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

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

258 259 260
        valid_roles = ('none', 'visitor', 'participant', 'moderator')

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

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

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

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

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

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

        self._text_buffer.add_message(
mathieui's avatar
mathieui committed
329 330
            "\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
331

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

366
    @refresh_wrapper.conditional
367
    def set_nick_color(self, nick: str, color: str) -> bool:
368 369 370 371 372 373 374 375
        "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
376 377
                self.core.information('Color for nick %s unset' % (nick),
                                      'Info')
378 379 380 381 382 383
        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
384
            nick_color_aliases = config.get_by_tabname('nick_color_aliases',
385
                                                       self.jid.bare)
386 387 388 389 390 391 392 393 394 395 396
            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
397

mathieui's avatar
mathieui committed
398 399 400 401 402 403
    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
404 405 406
        empty_after = empty_after or (
            self.input.get_text().startswith('/')
            and not self.input.get_text().startswith('//'))
mathieui's avatar
mathieui committed
407 408
        self.send_composing_chat_state(empty_after)
        return False
409

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

mathieui's avatar
mathieui committed
419 420
    def get_text_window(self):
        return self.text_win
421

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

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

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

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

mathieui's avatar
mathieui committed
504 505 506 507
    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
508
        from_nick, _, affiliation, show, status, role, jid, typ = dissect_presence(
mathieui's avatar
mathieui committed
509
            presence)
510 511
        if typ == 'unavailable':
            return
mathieui's avatar
mathieui committed
512
        user_color = self.search_for_color(from_nick)
mathieui's avatar
mathieui committed
513 514
        new_user = User(from_nick, affiliation, show, status, role, jid,
                        deterministic, user_color)
mathieui's avatar
mathieui committed
515 516 517 518 519 520 521
        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)
522

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

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

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

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

mathieui's avatar
mathieui committed
633 634 635
    def on_non_member_kicked(self):
        """We have been kicked because the MUC is members-only"""
        self.add_message(
mathieui's avatar
mathieui committed
636 637 638
            '\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
639 640 641 642 643 644
            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
645 646 647
            '\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
648 649 650
            typ=2)
        self.disconnect()

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

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

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

mathieui's avatar
mathieui committed
736
    def on_user_banned(self, presence, user, from_nick):
737
        """
mathieui's avatar
mathieui committed
738
        When someone is banned from a muc
739
        """
mathieui's avatar
mathieui committed
740 741 742 743 744 745 746 747 748
        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
749

750 751 752
        theme = get_theme()
        info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT)
        char_kick = theme.CHAR_KICK
753

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

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

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

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

926 927 928 929
            error_leave_txt = ''
            if server_initiated:
                error_leave_txt = ' due to an error'

mathieui's avatar
mathieui committed
930 931 932
            if not jid.full:
                leave_msg = ('\x19%(color_spec)s}%(spec)s \x19%(color)s}'
                             '%(nick)s\x19%(info_col)s} has left the '
933
                             'room%(error_leave)s') % {
mathieui's avatar
mathieui committed
934