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

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

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

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

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

27 28
from poezio import common
from poezio import fixes
29
from poezio import mam
30 31 32 33
from poezio import multiuserchat as muc
from poezio import timed_events
from poezio import windows
from poezio import xhtml
34
from poezio.common import safeJID, to_utc
35
from poezio.config import config
36
from poezio.core.structs import Command
37 38 39 40 41
from poezio.decorators import refresh_wrapper, command_args_parser
from poezio.logger import logger
from poezio.roster import roster
from poezio.theming import get_theme, dump_tuple
from poezio.user import User
42
from poezio.core.structs import Completion, Status
43 44 45 46 47 48 49 50
from poezio.ui.types import (
    BaseMessage,
    InfoMessage,
    Message,
    MucOwnJoinMessage,
    MucOwnLeaveMessage,
    StatusMessage,
)
51

52 53
log = logging.getLogger(__name__)

54
NS_MUC_USER = 'http://jabber.org/protocol/muc#user'
55
STATUS_XPATH = '{%s}x/{%s}status' % (NS_MUC_USER, NS_MUC_USER)
56

57 58
COMPARE_USERS_LAST_TALKED = lambda x: x.last_talked

59 60 61 62

class MucTab(ChatTab):
    """
    The tab containing a multi-user-chat room.
Maxime Buquet's avatar
Maxime Buquet committed
63
    It contains a userlist, an input, a topic, an information and a chat zone
64 65
    """
    message_type = 'groupchat'
66 67
    plugin_commands = {}  # type: Dict[str, Command]
    plugin_keys = {}  # type: Dict[str, Callable]
68
    additional_information = {}  # type: Dict[str, Callable[[str], str]]
69
    lagged = False
70

71
    def __init__(self, core, jid, nick, password=None):
72
        ChatTab.__init__(self, core, jid)
73 74 75
        self.joined = False
        self._state = 'disconnected'
        # our nick in the MUC
76
        self.own_nick = nick
77
        # self User object
78
        self.own_user = None  # type: Optional[User]
79
        self.password = password
80
        # buffered presences
81
        self.presence_buffer = []
82
        # userlist
83
        self.users = []  # type: List[User]
84
        # private conversations
85
        self.privates = []  # type: List[Tab]
86 87
        # Used to check if we are still receiving muc history
        self.last_message_was_history = None  # type: Optional[bool]
88
        self.topic = ''
89
        self.topic_from = ''
90 91 92
        # Self ping event, so we can cancel it when we leave the room
        self.self_ping_event = None
        # UI stuff
93 94 95 96 97
        self.topic_win = windows.Topic()
        self.v_separator = windows.VerticalSeparator()
        self.user_win = windows.UserList()
        self.info_header = windows.MucInfoWin()
        self.input = windows.MessageInput()
98
        # List of ignored users
99
        self.ignores = []  # type: List[User]
100
        # keys
101 102
        self.register_keys()
        self.update_keys()
103
        # commands
104
        self.register_commands()
105
        self.update_commands()
106
        self.resize()
107 108 109

    @property
    def general_jid(self):
110
        return self.jid
111

112
    def check_send_chat_state(self) -> bool:
mathieui's avatar
mathieui committed
113 114 115
        "If we should send a chat state"
        return self.joined

116
    @property
117
    def last_connection(self) -> Optional[datetime]:
118 119 120 121 122
        last_message = self._text_buffer.last_message
        if last_message:
            return last_message.time
        return None

123
    @staticmethod
124
    @refresh_wrapper.always
125 126 127 128 129 130 131
    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
132
    @refresh_wrapper.always
133 134 135 136 137 138
    def remove_information_element(plugin_name: str) -> None:
        """
        Lets a plugin add its own information to the MucInfoWin
        """
        del MucTab.additional_information[plugin_name]

139 140
    def cancel_config(self, form):
        """
Kim Alvefur's avatar
Kim Alvefur committed
141
        The user do not want to send their config, send an iq cancel
142
        """
143
        muc.cancel_config(self.core.xmpp, self.jid.bare)
144 145 146 147
        self.core.close_tab()

    def send_config(self, form):
        """
Kim Alvefur's avatar
Kim Alvefur committed
148
        The user sends their config to the server
149
        """
150
        muc.configure_room(self.core.xmpp, self.jid.bare, form)
151 152
        self.core.close_tab()

153 154 155 156 157 158
    def join(self):
        """
        Join the room
        """
        status = self.core.get_status()
        if self.last_connection:
159
            delta = to_utc(datetime.now()) - to_utc(self.last_connection)
160 161
            seconds = delta.seconds + delta.days * 24 * 3600
        else:
162 163 164 165
            last_message = self._text_buffer.find_last_message()
            seconds = None
            if last_message is not None:
                seconds = (datetime.now() - last_message.time).seconds
mathieui's avatar
mathieui committed
166 167
        muc.join_groupchat(
            self.core,
168
            self.jid.bare,
mathieui's avatar
mathieui committed
169 170 171 172 173
            self.own_nick,
            self.password,
            status=status.message,
            show=status.show,
            seconds=seconds)
174

175
    def leave_room(self, message: str):
mathieui's avatar
mathieui committed
176
        if self.joined:
177 178 179 180
            theme = get_theme()
            info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT)
            char_quit = theme.CHAR_QUIT
            spec_col = dump_tuple(theme.COLOR_QUIT_CHAR)
181

182 183
            if config.get_by_tabname('display_user_color_in_join_part',
                                     self.general_jid):
184
                color = dump_tuple(theme.COLOR_OWN_NICK)
185
            else:
186
                color = "3"
187

mathieui's avatar
mathieui committed
188
            if message:
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
                       ' (\x19o%(reason)s\x19%(info_col)s})') % {
mathieui's avatar
mathieui committed
193 194 195 196
                           'info_col': info_col,
                           'reason': message,
                           'spec': char_quit,
                           'color': color,
197 198 199
                           'color_spec': spec_col,
                           'nick': self.own_nick,
                       }
200
            else:
201 202
                msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
                       'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
203
                       ' left the room') % {
204
                           'info_col': info_col,
mathieui's avatar
mathieui committed
205 206
                           'spec': char_quit,
                           'color': color,
207 208 209
                           'color_spec': spec_col,
                           'nick': self.own_nick,
                       }
210
            self.add_message(MucOwnLeaveMessage(msg), typ=2)
211
            self.disconnect()
212
            muc.leave_groupchat(self.core.xmpp, self.jid.bare, self.own_nick,
mathieui's avatar
mathieui committed
213
                                message)
214
            self.core.disable_private_tabs(self.jid.bare, reason=msg)
215
        else:
216
            muc.leave_groupchat(self.core.xmpp, self.jid.bare, self.own_nick,
mathieui's avatar
mathieui committed
217
                                message)
mathieui's avatar
mathieui committed
218

219 220 221 222
    def change_affiliation(self,
                           nick_or_jid: Union[str, JID],
                           affiliation: str,
                           reason=''):
223 224 225
        """
        Change the affiliation of a nick or JID
        """
mathieui's avatar
mathieui committed
226

227 228
        def callback(iq):
            if iq['type'] == 'error':
mathieui's avatar
mathieui committed
229 230 231 232
                self.core.information(
                    "Could not set affiliation '%s' for '%s'." %
                    (affiliation, nick_or_jid), "Warning")

233 234 235 236 237
        if not self.joined:
            return

        valid_affiliations = ('outcast', 'none', 'member', 'admin', 'owner')
        if affiliation not in valid_affiliations:
mathieui's avatar
mathieui committed
238 239 240
            return self.core.information(
                'The affiliation must be one of ' +
                ', '.join(valid_affiliations), 'Error')
241
        if nick_or_jid in [user.nick for user in self.users]:
mathieui's avatar
mathieui committed
242
            muc.set_user_affiliation(
mathieui's avatar
mathieui committed
243
                self.core.xmpp,
244
                self.jid.bare,
mathieui's avatar
mathieui committed
245 246 247 248
                affiliation,
                nick=nick_or_jid,
                callback=callback,
                reason=reason)
249
        else:
mathieui's avatar
mathieui committed
250
            muc.set_user_affiliation(
mathieui's avatar
mathieui committed
251
                self.core.xmpp,
252
                self.jid.bare,
mathieui's avatar
mathieui committed
253 254 255 256
                affiliation,
                jid=safeJID(nick_or_jid),
                callback=callback,
                reason=reason)
257

258
    def change_role(self, nick: str, role: str, reason=''):
259 260 261
        """
        Change the role of a nick
        """
mathieui's avatar
mathieui committed
262

263 264
        def callback(iq):
            if iq['type'] == 'error':
mathieui's avatar
mathieui committed
265 266 267
                self.core.information(
                    "Could not set role '%s' for '%s'." % (role, nick),
                    "Warning")
mathieui's avatar
mathieui committed
268

269 270 271
        valid_roles = ('none', 'visitor', 'participant', 'moderator')

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

275 276 277 278
        try:
            target_jid = copy(self.jid)
            target_jid.resource = nick
        except InvalidJID:
279
            return self.core.information('Invalid nick', 'Info')
280

mathieui's avatar
mathieui committed
281
        muc.set_user_role(
282
            self.core.xmpp, self.jid.bare, nick, reason, role, callback=callback)
283

mathieui's avatar
mathieui committed
284
    @refresh_wrapper.conditional
285
    def print_info(self, nick: str) -> bool:
mathieui's avatar
mathieui committed
286 287 288 289 290 291 292 293 294
        """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
295
                inf, dump_tuple(theme.COLOR_MUC_JID), user.jid, inf)
mathieui's avatar
mathieui committed
296 297 298 299 300 301
        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
302 303 304 305 306 307 308 309 310 311 312
                    '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 ''
                }
313
        self.add_message(InfoMessage(info), typ=0)
mathieui's avatar
mathieui committed
314 315
        return True

316
    def change_topic(self, topic: str):
mathieui's avatar
mathieui committed
317
        """Change the current topic"""
318
        muc.change_subject(self.core.xmpp, self.jid.bare, topic)
mathieui's avatar
mathieui committed
319 320 321 322 323 324

    @refresh_wrapper.always
    def show_topic(self):
        """
        Print the current topic
        """
325 326 327
        theme = get_theme()
        info_text = dump_tuple(theme.COLOR_INFORMATION_TEXT)
        norm_text = dump_tuple(theme.COLOR_NORMAL_TEXT)
mathieui's avatar
mathieui committed
328 329 330 331
        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
332 333
                user_string = '\x19%s}(set by \x19%s}%s\x19%s})' % (
                    info_text, user_text, user.nick, info_text)
mathieui's avatar
mathieui committed
334 335 336 337 338
            else:
                user_string = self.topic_from
        else:
            user_string = ''

339 340 341 342 343 344 345
        self.add_message(
            InfoMessage(
                "The subject of the room is: \x19%s}%s %s" %
                (norm_text, self.topic, user_string),
            ),
            typ=0,
        )
mathieui's avatar
mathieui committed
346

mathieui's avatar
mathieui committed
347 348 349
    @refresh_wrapper.always
    def recolor(self, random_colors=False):
        """Recolor the current MUC users"""
mathieui's avatar
mathieui committed
350
        deterministic = config.get_by_tabname('deterministic_nick_colors',
351
                                              self.jid.bare)
mathieui's avatar
mathieui committed
352 353 354 355 356 357 358 359 360 361 362
        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
363
        sorted_users = sorted(self.users, key=COMPARE_USERS_LAST_TALKED, reverse=True)
mathieui's avatar
mathieui committed
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380
        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)

381
    @refresh_wrapper.conditional
382
    def set_nick_color(self, nick: str, color: str) -> bool:
383 384 385 386 387 388 389 390
        "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
391 392
                self.core.information('Color for nick %s unset' % (nick),
                                      'Info')
393 394 395 396 397 398
        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
399
            nick_color_aliases = config.get_by_tabname('nick_color_aliases',
400
                                                       self.jid.bare)
401 402 403 404 405 406 407 408 409 410 411
            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
412

mathieui's avatar
mathieui committed
413 414 415 416 417 418
    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
419 420 421
        empty_after = empty_after or (
            self.input.get_text().startswith('/')
            and not self.input.get_text().startswith('//'))
mathieui's avatar
mathieui committed
422 423
        self.send_composing_chat_state(empty_after)
        return False
424

425
    def get_nick(self) -> str:
426
        if config.get('show_muc_jid'):
427 428
            return self.jid.bare
        bookmark = self.core.bookmarks[self.jid.bare]
429 430 431
        if bookmark is not None and bookmark.name:
            return bookmark.name
        # TODO: send the disco#info identity name here, if it exists.
432
        return self.jid.user
433

mathieui's avatar
mathieui committed
434 435
    def get_text_window(self):
        return self.text_win
436

mathieui's avatar
mathieui committed
437 438 439 440
    def on_lose_focus(self):
        if self.joined:
            if self.input.text:
                self.state = 'nonempty'
441 442
            elif self.lagged:
                self.state = 'disconnected'
mathieui's avatar
mathieui committed
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458
            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
459 460
        if self.joined and config.get_by_tabname(
                'send_chat_states',
mathieui's avatar
mathieui committed
461 462 463 464
                self.general_jid) and not self.input.get_text():
            self.send_chat_state('active')

    def handle_presence(self, presence):
465
        """
mathieui's avatar
mathieui committed
466
        Handle MUC presence
467
        """
468
        self.reset_lag()
mathieui's avatar
mathieui committed
469 470 471
        status_codes = set()
        for status_code in presence.xml.findall(STATUS_XPATH):
            status_codes.add(status_code.attrib['code'])
mathieui's avatar
mathieui committed
472
        if presence['type'] == 'error':
473
            self.core.room_error(presence, self.jid.bare)
mathieui's avatar
mathieui committed
474
        elif not self.joined:
475
            own = '110' in status_codes
476 477
            if own or len(self.presence_buffer) >= 10:
                self.process_presence_buffer(presence, own)
mathieui's avatar
mathieui committed
478 479 480 481 482 483 484 485
            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)
486
        if self.core.tabs.current_tab is self:
mathieui's avatar
mathieui committed
487 488
            self.text_win.refresh()
            self.user_win.refresh_if_changed(self.users)
489 490 491
            self.info_header.refresh(
                self, self.text_win, user=self.own_user,
                information=MucTab.additional_information)
mathieui's avatar
mathieui committed
492 493
            self.input.refresh()
            self.core.doupdate()
494

495
    def process_presence_buffer(self, last_presence, own):
496
        """
mathieui's avatar
mathieui committed
497
        Batch-process all the initial presences
498
        """
mathieui's avatar
mathieui committed
499
        deterministic = config.get_by_tabname('deterministic_nick_colors',
500
                                              self.jid.bare)
mathieui's avatar
mathieui committed
501

mathieui's avatar
mathieui committed
502 503 504 505 506
        for stanza in self.presence_buffer:
            try:
                self.handle_presence_unjoined(stanza, deterministic)
            except PresenceError:
                self.core.room_error(stanza, stanza['from'].bare)
507 508
        self.presence_buffer = []
        self.handle_presence_unjoined(last_presence, deterministic, own)
mathieui's avatar
mathieui committed
509 510 511
        self.users.sort()
        # Enable the self ping event, to regularly check if we
        # are still in the room.
512 513
        if own:
            self.enable_self_ping_event()
514
        if self.core.tabs.current_tab is not self:
mathieui's avatar
mathieui committed
515
            self.refresh_tab_win()
516
            self.core.tabs.current_tab.refresh_input()
mathieui's avatar
mathieui committed
517
            self.core.doupdate()
518

519
    def handle_presence_unjoined(self, presence: Presence, deterministic, own=False) -> None:
mathieui's avatar
mathieui committed
520 521 522
        """
        Presence received while we are not in the room (before code=110)
        """
523 524 525
        # If presence is coming from MUC barejid, ignore.
        if not presence['from'].resource:
            return None
526 527
        dissected_presence = dissect_presence(presence)
        from_nick, _, affiliation, show, status, role, jid, typ = dissected_presence
528 529
        if typ == 'unavailable':
            return
mathieui's avatar
mathieui committed
530
        user_color = self.search_for_color(from_nick)
mathieui's avatar
mathieui committed
531 532
        new_user = User(from_nick, affiliation, show, status, role, jid,
                        deterministic, user_color)
mathieui's avatar
mathieui committed
533 534 535 536 537 538 539
        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)
540

541
    def own_join(self, from_nick: str, new_user: User, status_codes: Set[str]):
542
        """
mathieui's avatar
mathieui committed
543
        Handle the last presence we received, entering the room
544
        """
mathieui's avatar
mathieui committed
545 546 547
        self.own_nick = from_nick
        self.own_user = new_user
        self.joined = True
548 549
        if self.jid.bare in self.core.initial_joins:
            self.core.initial_joins.remove(self.jid.bare)
mathieui's avatar
mathieui committed
550
            self._state = 'normal'
551
        elif self != self.core.tabs.current_tab:
mathieui's avatar
mathieui committed
552
            self._state = 'joined'
553
        if (self.core.tabs.current_tab is self
mathieui's avatar
mathieui committed
554 555
                and self.core.status.show not in ('xa', 'away')):
            self.send_chat_state('active')
556 557
        theme = get_theme()
        new_user.color = theme.COLOR_OWN_NICK
558

mathieui's avatar
mathieui committed
559 560 561 562
        if config.get_by_tabname('display_user_color_in_join_part',
                                 self.general_jid):
            color = dump_tuple(new_user.color)
        else:
563
            color = "3"
564

565 566 567
        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
568 569 570 571
        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,
572
                              'spec': theme.CHAR_JOIN,
mathieui's avatar
mathieui committed
573 574 575 576
                              'color_spec': spec_col,
                              'nick_col': color,
                              'info_col': info_col,
                          }
577
        self.add_message(MucOwnJoinMessage(enable_message), typ=2)
578
        self.core.enable_private_tabs(self.jid.bare, enable_message)
mathieui's avatar
mathieui committed
579 580
        if '201' in status_codes:
            self.add_message(
581 582 583
                InfoMessage('Info: The room has been created'),
                typ=0
            )
mathieui's avatar
mathieui committed
584 585
        if '170' in status_codes:
            self.add_message(
586 587 588 589 590 591 592
                InfoMessage(
                    '\x19%(warn_col)s}Warning:\x19%(info_col)s}'
                    ' This room is publicly logged' % {
                        'info_col': info_col,
                        'warn_col': warn_col
                    },
                ),
mathieui's avatar
mathieui committed
593 594 595
                typ=0)
        if '100' in status_codes:
            self.add_message(
596 597 598 599 600 601 602
                InfoMessage(
                    '\x19%(warn_col)s}Warning:\x19%(info_col)s}'
                    ' This room is not anonymous.' % {
                        'info_col': info_col,
                        'warn_col': warn_col
                    },
                ),
mathieui's avatar
mathieui committed
603
                typ=0)
604
        mam.schedule_tab_open(self)
605

606
    def handle_presence_joined(self, presence: Presence, status_codes) -> None:
607
        """
mathieui's avatar
mathieui committed
608
        Handle new presences when we are already in the room
609
        """
610 611 612
        # If presence is coming from MUC barejid, ignore.
        if not presence['from'].resource:
            return None
613 614
        dissected_presence = dissect_presence(presence)
        from_nick, from_room, affiliation, show, status, role, jid, typ = dissected_presence
mathieui's avatar
mathieui committed
615 616 617 618
        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'
619
        server_initiated = '333' in status_codes and typ == 'unavailable'
mathieui's avatar
mathieui committed
620 621 622 623 624 625
        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
626 627
            self.on_user_join(from_nick, affiliation, show, status, role, jid,
                              user_color)
mathieui's avatar
mathieui committed
628
        elif user is None:
629
            log.error('BUG: User %s in %s is None', from_nick, self.jid.bare)
mathieui's avatar
mathieui committed
630 631 632 633 634 635
            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
636 637
            self.core.on_user_left_private_conversation(
                from_room, user, status)
mathieui's avatar
mathieui committed
638
            self.on_user_banned(presence, user, from_nick)
639
        elif kick and not server_initiated:
mathieui's avatar
mathieui committed
640
            self.core.events.trigger('muc_kick', presence, self)
mathieui's avatar
mathieui committed
641 642
            self.core.on_user_left_private_conversation(
                from_room, user, status)
mathieui's avatar
mathieui committed
643 644 645 646 647 648 649 650 651
            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
652
            self.on_user_leave_groupchat(user, jid, status, from_nick,
653
                                         from_room, server_initiated)
mathieui's avatar
mathieui committed
654
        # status change
655
        else:
mathieui's avatar
mathieui committed
656 657
            self.on_user_change_status(user, from_nick, from_room, affiliation,
                                       role, show, status)
658

mathieui's avatar
mathieui committed
659 660 661
    def on_non_member_kicked(self):
        """We have been kicked because the MUC is members-only"""
        self.add_message(
662
            MucOwnLeaveMessage(
663 664 665
                'You have been kicked because you '
                'are not a member and the room is now members-only.'
            ),
mathieui's avatar
mathieui committed
666 667 668 669 670 671
            typ=2)
        self.disconnect()

    def on_muc_shutdown(self):
        """We have been kicked because the MUC service is shutting down"""
        self.add_message(
672
            MucOwnLeaveMessage(
673 674 675
                'You have been kicked because the'
                ' MUC service is shutting down.'
            ),
mathieui's avatar
mathieui committed
676 677 678
            typ=2)
        self.disconnect()

mathieui's avatar
mathieui committed
679 680
    def on_user_join(self, from_nick, affiliation, show, status, role, jid,
                     color):
681
        """
mathieui's avatar
mathieui committed
682
        When a new user joins the groupchat
683
        """
mathieui's avatar
mathieui committed
684
        deterministic = config.get_by_tabname('deterministic_nick_colors',
685
                                              self.jid.bare)
mathieui's avatar
mathieui committed
686 687
        user = User(from_nick, affiliation, show, status, role, jid,
                    deterministic, color)
mathieui's avatar
mathieui committed
688 689 690 691 692 693 694 695 696
        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
697 698 699 700
            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
701 702 703
            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
704 705 706 707 708 709
                           'nick': from_nick,
                           'spec': char_join,
                           'color': color,
                           'info_col': info_col,
                           'color_spec': spec_col,
                       }
mathieui's avatar
mathieui committed
710 711 712 713
            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
714 715 716 717 718
                           'spec': char_join,
                           'nick': from_nick,
                           'color': color,
                           'jid': jid.full,
                           'info_col': info_col,
719
                           'jid_color': dump_tuple(theme.COLOR_MUC_JID),
mathieui's avatar
mathieui committed
720 721
                           'color_spec': spec_col,
                       }
722
            self.add_message(InfoMessage(msg), typ=2)
723
        self.core.on_user_rejoined_private_conversation(self.jid.bare, from_nick)
mathieui's avatar
mathieui committed
724 725

    def on_user_nick_change(self, presence, user, from_nick, from_room):
mathieui's avatar
mathieui committed
726 727
        new_nick = presence.xml.find(
            '{%s}x/{%s}item' % (NS_MUC_USER, NS_MUC_USER)).attrib['nick']
728
        old_color = user.color
mathieui's avatar
mathieui committed
729 730 731 732
        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)
733
            user.change_nick(new_nick)
734
        else:
735
            user.change_nick(new_nick)
mathieui's avatar
mathieui committed
736
            deterministic = config.get_by_tabname('deterministic_nick_colors',
737
                                                  self.jid.bare)
738 739
            color = config.get_by_tabname(new_nick, 'muc_colors') or None
            if color or deterministic:
mathieui's avatar
mathieui committed
740 741 742
                user.change_color(color, deterministic)
        self.users.remove(user)
        bisect.insort_left(self.users, user)
743

mathieui's avatar
mathieui committed
744 745 746
        if config.get_by_tabname('display_user_color_in_join_part',
                                 self.general_jid):
            color = dump_tuple(user.color)
747
            old_color = dump_tuple(old_color)
748
        else:
749
            old_color = color = 3
mathieui's avatar
mathieui committed
750
        info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
mathieui's avatar
mathieui committed
751
        self.add_message(
752 753 754 755 756 757 758 759 760 761
            InfoMessage(
                '\x19%(old_color)s}%(old)s\x19%(info_col)s} is'
                ' now known as \x19%(color)s}%(new)s' % {
                    'old': from_nick,
                    'new': new_nick,
                    'color': color,
                    'old_color': old_color,
                    'info_col': info_col
                },
            ),
mathieui's avatar
mathieui committed
762
            typ=2)
mathieui's avatar
mathieui committed
763
        # rename the private tabs if needed
764
        self.core.rename_private_tabs(self.jid.bare, from_nick, user)
765

mathieui's avatar
mathieui committed
766
    def on_user_banned(self, presence, user, from_nick):
767
        """
mathieui's avatar
mathieui committed
768
        When someone is banned from a muc
769
        """
770
        cls = InfoMessage
mathieui's avatar
mathieui committed
771 772 773 774 775 776 777 778 779
        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
780

781 782 783
        theme = get_theme()
        info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT)
        char_kick = theme.CHAR_KICK
784

mathieui's avatar
mathieui committed
785
        if from_nick == self.own_nick:  # we are banned
786
            cls = MucOwnLeaveMessage
mathieui's avatar
mathieui committed
787 788 789
            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
790 791 792 793
                                'spec': char_kick,
                                'by': by,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
794 795 796
            else:
                kick_msg = ('\x191}%(spec)s \x193}You\x19'
                            '%(info_col)s} have been banned.') % {
mathieui's avatar
mathieui committed
797 798 799
                                'spec': char_kick,
                                'info_col': info_col
                            }
800
            self.core.disable_private_tabs(self.jid.bare, reason=kick_msg)
mathieui's avatar
mathieui committed
801 802
            self.disconnect()
            self.refresh_tab_win()
803
            self.core.tabs.current_tab.refresh_input()
mathieui's avatar
mathieui committed
804 805 806 807 808
            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:
809
                    muc.join_groupchat(self.core, self.jid.bare, self.own_nick)
mathieui's avatar
mathieui committed
810
                else:
mathieui's avatar
mathieui committed
811 812
                    self.core.add_timed_event(
                        timed_events.DelayedEvent(delay, muc.join_groupchat,
813
                                                  self.core, self.jid.bare,
mathieui's avatar
mathieui committed
814
                                                  self.own_nick))
815

mathieui's avatar
mathieui committed
816 817 818 819 820 821 822 823 824 825 826
        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
827 828 829 830 831 832
                                'spec': char_kick,
                                'nick': from_nick,
                                'color': color,
                                'by': by,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
833 834 835
            else:
                kick_msg = ('\x191}%(spec)s \x19%(color)s}%(nick)s'
                            '\x19%(info_col)s} has been banned') % {
mathieui's avatar
mathieui committed
836 837 838 839 840
                                'spec': char_kick,
                                'nick': from_nick,
                                'color': color,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
841 842 843
        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
844 845 846
                             'reason': reason.text,
                             'info_col': info_col
                         }
847
        self.add_message(cls(kick_msg), typ=2)
mathieui's avatar
mathieui committed
848 849

    def on_user_kicked(self, presence, user, from_nick):
850
        """
mathieui's avatar
mathieui committed
851
        When someone is kicked from a muc
852
        """
853
        cls = InfoMessage
mathieui's avatar
mathieui committed
854 855 856 857 858 859
        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
860 861 862
        theme = get_theme()
        info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT)
        char_kick = theme.CHAR_KICK
mathieui's avatar
mathieui committed
863 864
        if actor_elem is not None:
            by = actor_elem.get('nick') or actor_elem.get('jid')
mathieui's avatar
mathieui committed
865
        if from_nick == self.own_nick:  # we are kicked
866
            cls = MucOwnLeaveMessage
mathieui's avatar
mathieui committed
867 868 869 870
            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
871 872 873 874
                                'spec': char_kick,
                                'by': by,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
875 876 877
            else:
                kick_msg = ('\x191}%(spec)s \x193}You\x19%(info_col)s}'
                            ' have been kicked.') % {
mathieui's avatar
mathieui committed
878 879 880
                                'spec': char_kick,
                                'info_col': info_col
                            }
881
            self.core.disable_private_tabs(self.jid.bare, reason=kick_msg)
mathieui's avatar
mathieui committed
882 883
            self.disconnect()
            self.refresh_tab_win()
884
            self.core.tabs.current_tab.refresh_input()
mathieui's avatar
mathieui committed
885 886 887 888 889 890
            # 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:
891
                    muc.join_groupchat(self.core, self.jid.bare, self.own_nick)
mathieui's avatar
mathieui committed
892
                else:
mathieui's avatar
mathieui committed
893 894
                    self.core.add_timed_event(
                        timed_events.DelayedEvent(delay, muc.join_groupchat,
895
                                                  self.core, self.jid.bare,
mathieui's avatar
mathieui committed
896
                                                  self.own_nick))
mathieui's avatar
mathieui committed
897 898 899 900 901 902 903 904 905 906
        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
907 908 909 910 911 912
                                'spec': char_kick,
                                'nick': from_nick,
                                'color': color,
                                'by': by,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
913 914 915
            else:
                kick_msg = ('\x191}%(spec)s \x19%(color)s}%(nick)s'
                            '\x19%(info_col)s} has been kicked') % {
mathieui's avatar
mathieui committed
916 917 918 919 920
                                'spec': char_kick,
                                'nick': from_nick,
                                'color': color,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
921 922 923
        if reason is not None and reason.text:
            kick_msg += ('\x19%(info_col)s} Reason: \x196}'
                         '%(reason)s') % {
mathieui's avatar
mathieui committed
924 925 926
                             'reason': reason.text,
                             'info_col': info_col
                         }
927
        self.add_message(cls(kick_msg), typ=2)