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

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

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

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

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

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

45 46
log = logging.getLogger(__name__)

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

50 51
COMPARE_USERS_LAST_TALKED = lambda x: x.last_talked

52 53 54 55

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

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

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

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

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

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

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

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

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

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

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

mathieui's avatar
mathieui committed
179
            if message:
180 181
                msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
                       'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
182
                       ' left the room'
183
                       ' (\x19o%(reason)s\x19%(info_col)s})') % {
mathieui's avatar
mathieui committed
184 185 186 187
                           'info_col': info_col,
                           'reason': message,
                           'spec': char_quit,
                           'color': color,
188 189 190
                           'color_spec': spec_col,
                           'nick': self.own_nick,
                       }
191
            else:
192 193
                msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
                       'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
194
                       ' left the room') % {
195
                           'info_col': info_col,
mathieui's avatar
mathieui committed
196 197
                           'spec': char_quit,
                           'color': color,
198 199 200
                           'color_spec': spec_col,
                           'nick': self.own_nick,
                       }
201
            self.add_message(InfoMessage(msg), typ=2)
202
            self.disconnect()
203
            muc.leave_groupchat(self.core.xmpp, self.jid.bare, self.own_nick,
mathieui's avatar
mathieui committed
204
                                message)
205
            self.core.disable_private_tabs(self.jid.bare, reason=msg)
206
        else:
207
            muc.leave_groupchat(self.core.xmpp, self.jid.bare, self.own_nick,
mathieui's avatar
mathieui committed
208
                                message)
mathieui's avatar
mathieui committed
209

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

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

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

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

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

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

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

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

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

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

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

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

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

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

330 331 332 333 334 335 336
        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
337

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

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

mathieui's avatar
mathieui committed
404 405 406 407 408 409
    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
410 411 412
        empty_after = empty_after or (
            self.input.get_text().startswith('/')
            and not self.input.get_text().startswith('//'))
mathieui's avatar
mathieui committed
413 414
        self.send_composing_chat_state(empty_after)
        return False
415

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

mathieui's avatar
mathieui committed
425 426
    def get_text_window(self):
        return self.text_win
427

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

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

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

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

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

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

mathieui's avatar
mathieui committed
547 548 549 550
        if config.get_by_tabname('display_user_color_in_join_part',
                                 self.general_jid):
            color = dump_tuple(new_user.color)
        else:
551
            color = "3"
552

553 554 555
        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
556 557 558 559
        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,
560
                              'spec': theme.CHAR_JOIN,
mathieui's avatar
mathieui committed
561 562 563 564
                              'color_spec': spec_col,
                              'nick_col': color,
                              'info_col': info_col,
                          }
565
        self.add_message(InfoMessage(enable_message), typ=2)
566
        self.core.enable_private_tabs(self.jid.bare, enable_message)
mathieui's avatar
mathieui committed
567 568
        if '201' in status_codes:
            self.add_message(
569 570 571
                InfoMessage('Info: The room has been created'),
                typ=0
            )
mathieui's avatar
mathieui committed
572 573
        if '170' in status_codes:
            self.add_message(
574 575 576 577 578 579 580
                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
581 582 583
                typ=0)
        if '100' in status_codes:
            self.add_message(
584 585 586 587 588 589 590
                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
591
                typ=0)
592

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

mathieui's avatar
mathieui committed
643 644 645
    def on_non_member_kicked(self):
        """We have been kicked because the MUC is members-only"""
        self.add_message(
646 647 648 649
            InfoMessage(
                'You have been kicked because you '
                'are not a member and the room is now members-only.'
            ),
mathieui's avatar
mathieui committed
650 651 652 653 654 655
            typ=2)
        self.disconnect()

    def on_muc_shutdown(self):
        """We have been kicked because the MUC service is shutting down"""
        self.add_message(
656 657 658 659
            InfoMessage(
                'You have been kicked because the'
                ' MUC service is shutting down.'
            ),
mathieui's avatar
mathieui committed
660 661 662
            typ=2)
        self.disconnect()

mathieui's avatar
mathieui committed
663 664
    def on_user_join(self, from_nick, affiliation, show, status, role, jid,
                     color):
665
        """
mathieui's avatar
mathieui committed
666
        When a new user joins the groupchat
667
        """
mathieui's avatar
mathieui committed
668
        deterministic = config.get_by_tabname('deterministic_nick_colors',
669
                                              self.jid.bare)
mathieui's avatar
mathieui committed
670 671
        user = User(from_nick, affiliation, show, status, role, jid,
                    deterministic, color)
mathieui's avatar
mathieui committed
672 673 674 675 676 677 678 679 680
        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
681 682 683 684
            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
685 686 687
            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
688 689 690 691 692 693
                           'nick': from_nick,
                           'spec': char_join,
                           'color': color,
                           'info_col': info_col,
                           'color_spec': spec_col,
                       }
mathieui's avatar
mathieui committed
694 695 696 697
            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
698 699 700 701 702
                           'spec': char_join,
                           'nick': from_nick,
                           'color': color,
                           'jid': jid.full,
                           'info_col': info_col,
703
                           'jid_color': dump_tuple(theme.COLOR_MUC_JID),
mathieui's avatar
mathieui committed
704 705
                           'color_spec': spec_col,
                       }
706
            self.add_message(InfoMessage(msg), typ=2)
707
        self.core.on_user_rejoined_private_conversation(self.jid.bare, from_nick)
mathieui's avatar
mathieui committed
708 709

    def on_user_nick_change(self, presence, user, from_nick, from_room):
mathieui's avatar
mathieui committed
710 711
        new_nick = presence.xml.find(
            '{%s}x/{%s}item' % (NS_MUC_USER, NS_MUC_USER)).attrib['nick']
712
        old_color = user.color
mathieui's avatar
mathieui committed
713 714 715 716
        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)
717
            user.change_nick(new_nick)
718
        else:
719
            user.change_nick(new_nick)
mathieui's avatar
mathieui committed
720
            deterministic = config.get_by_tabname('deterministic_nick_colors',
721
                                                  self.jid.bare)
722 723
            color = config.get_by_tabname(new_nick, 'muc_colors') or None
            if color or deterministic:
mathieui's avatar
mathieui committed
724 725 726
                user.change_color(color, deterministic)
        self.users.remove(user)
        bisect.insort_left(self.users, user)
727

mathieui's avatar
mathieui committed
728 729 730
        if config.get_by_tabname('display_user_color_in_join_part',
                                 self.general_jid):
            color = dump_tuple(user.color)
731
            old_color = dump_tuple(old_color)
732
        else:
733
            old_color = color = 3
mathieui's avatar
mathieui committed
734
        info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
mathieui's avatar
mathieui committed
735
        self.add_message(
736 737 738 739 740 741 742 743 744 745
            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
746
            typ=2)
mathieui's avatar
mathieui committed
747
        # rename the private tabs if needed
748
        self.core.rename_private_tabs(self.jid.bare, from_nick, user)
749

mathieui's avatar
mathieui committed
750
    def on_user_banned(self, presence, user, from_nick):
751
        """
mathieui's avatar
mathieui committed
752
        When someone is banned from a muc
753
        """
mathieui's avatar
mathieui committed
754 755 756 757 758 759 760 761 762
        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
763

764 765 766
        theme = get_theme()
        info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT)
        char_kick = theme.CHAR_KICK
767

mathieui's avatar
mathieui committed
768
        if from_nick == self.own_nick:  # we are banned
mathieui's avatar
mathieui committed
769 770 771
            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
772 773 774 775
                                'spec': char_kick,
                                'by': by,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
776 777 778
            else:
                kick_msg = ('\x191}%(spec)s \x193}You\x19'
                            '%(info_col)s} have been banned.') % {
mathieui's avatar
mathieui committed
779 780 781
                                'spec': char_kick,
                                'info_col': info_col
                            }
782
            self.core.disable_private_tabs(self.jid.bare, reason=kick_msg)
mathieui's avatar
mathieui committed
783 784
            self.disconnect()
            self.refresh_tab_win()
785
            self.core.tabs.current_tab.refresh_input()
mathieui's avatar
mathieui committed
786 787 788 789 790
            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:
791
                    muc.join_groupchat(self.core, self.jid.bare, self.own_nick)
mathieui's avatar
mathieui committed
792
                else:
mathieui's avatar
mathieui committed
793 794
                    self.core.add_timed_event(
                        timed_events.DelayedEvent(delay, muc.join_groupchat,
795
                                                  self.core, self.jid.bare,
mathieui's avatar
mathieui committed
796
                                                  self.own_nick))
797

mathieui's avatar
mathieui committed
798 799 800 801 802 803 804 805 806 807 808
        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
809 810 811 812 813 814
                                'spec': char_kick,
                                'nick': from_nick,
                                'color': color,
                                'by': by,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
815 816 817
            else:
                kick_msg = ('\x191}%(spec)s \x19%(color)s}%(nick)s'
                            '\x19%(info_col)s} has been banned') % {
mathieui's avatar
mathieui committed
818 819 820 821 822
                                'spec': char_kick,
                                'nick': from_nick,
                                'color': color,
                                'info_col': info_col
                            }
mathieui's avatar
mathieui committed
823 824 825
        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
826 827 828
                             'reason': reason.text,
                             'info_col': info_col
                         }
829
        self.add_message(InfoMessage(kick_msg), typ=2)
mathieui's avatar
mathieui committed
830 831

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

mathieui's avatar
mathieui committed
909
    def on_user_leave_groupchat(self,
910 911 912 913 914
                                user: User,
                                jid: JID,
                                status: str,
                                from_nick: str,
                                from_room: JID,
915
                                server_initiated=False):
mathieui's avatar
mathieui committed
916
        """
Maxime Buquet's avatar
Maxime Buquet committed
917
        When a user leaves a groupchat
mathieui's avatar
mathieui committed
918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935
        """
        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
936 937 938
            theme = get_theme()
            info_col = dump_tuple(theme.COLOR_INFORMATION_TEXT)
            spec_col = dump_tuple(theme.COLOR_QUIT_CHAR)
mathieui's avatar
mathieui committed
939

940