muctab.py 71.3 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 11 12
import logging
log = logging.getLogger(__name__)

13
import bisect
14 15 16
import curses
import os
import random
17
import re
mathieui's avatar
mathieui committed
18
from datetime import datetime
19

mathieui's avatar
mathieui committed
20
from poezio.tabs import ChatTab, Tab
21

22 23 24 25 26 27 28 29 30 31 32 33 34
from poezio import common
from poezio import fixes
from poezio import multiuserchat as muc
from poezio import timed_events
from poezio import windows
from poezio import xhtml
from poezio.common import safeJID
from poezio.config import config
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
35 36 37


SHOW_NAME = {
38 39 40 41 42
    'dnd': 'busy',
    'away': 'away',
    'xa': 'not available',
    'chat': 'chatty',
    '': 'available'
43 44 45 46 47 48 49 50 51 52 53 54 55
    }

NS_MUC_USER = 'http://jabber.org/protocol/muc#user'


class MucTab(ChatTab):
    """
    The tab containing a multi-user-chat room.
    It contains an userlist, an input, a topic, an information and a chat zone
    """
    message_type = 'groupchat'
    plugin_commands = {}
    plugin_keys = {}
56
    def __init__(self, core, jid, nick, password=None):
57
        self.joined = False
58
        ChatTab.__init__(self, core, jid)
59 60 61
        if self.joined == False:
            self._state = 'disconnected'
        self.own_nick = nick
62
        self.own_user = None
63
        self.name = jid
64
        self.password = password
65 66 67
        self.users = []
        self.privates = [] # private conversations
        self.topic = ''
68
        self.topic_from = ''
69
        self.remote_wants_chatstates = True
70 71
        # Self ping event, so we can cancel it when we leave the room
        self.self_ping_event = None
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
        # We send active, composing and paused states to the MUC because
        # the chatstate may or may not be filtered by the MUC,
        # that’s not our problem.
        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()
        self.ignores = []       # set of Users
        # keys
        self.key_func['^I'] = self.completion
        self.key_func['M-u'] = self.scroll_user_list_down
        self.key_func['M-y'] = self.scroll_user_list_up
        self.key_func['M-n'] = self.go_to_next_hl
        self.key_func['M-p'] = self.go_to_prev_hl
        # commands
        self.register_command('ignore', self.command_ignore,
91 92 93
                usage='<nickname>',
                desc='Ignore a specified nickname.',
                shortdesc='Ignore someone',
94 95
                completion=self.completion_ignore)
        self.register_command('unignore', self.command_unignore,
96 97 98
                usage='<nickname>',
                desc='Remove the specified nickname from the ignore list.',
                shortdesc='Unignore someone.',
99 100
                completion=self.completion_unignore)
        self.register_command('kick', self.command_kick,
101 102 103 104
                usage='<nick> [reason]',
                desc='Kick the user with the specified nickname.'
                     ' You also can give an optional reason.',
                shortdesc='Kick someone.',
105 106
                completion=self.completion_quoted)
        self.register_command('ban', self.command_ban,
107 108 109
                usage='<nick> [reason]',
                desc='Ban the user with the specified nickname.'
                     ' You also can give an optional reason.',
110 111 112
                shortdesc='Ban someone',
                completion=self.completion_quoted)
        self.register_command('role', self.command_role,
113 114 115 116 117
                usage='<nick> <role> [reason]',
                desc='Set the role of an user. Roles can be:'
                     ' none, visitor, participant, moderator.'
                     ' You also can give an optional reason.',
                shortdesc='Set the role of an user.',
118 119
                completion=self.completion_role)
        self.register_command('affiliation', self.command_affiliation,
120 121 122 123
                usage='<nick or jid> <affiliation>',
                desc='Set the affiliation of an user. Affiliations can be:'
                     ' outcast, none, member, admin, owner.',
                shortdesc='Set the affiliation of an user.',
124 125
                completion=self.completion_affiliation)
        self.register_command('topic', self.command_topic,
126 127 128
                usage='<subject>',
                desc='Change the subject of the room.',
                shortdesc='Change the subject.',
129 130
                completion=self.completion_topic)
        self.register_command('query', self.command_query,
131 132 133 134 135 136
                usage='<nick> [message]',
                desc='Open a private conversation with <nick>. This nick'
                     ' has to be present in the room you\'re currently in.'
                     ' If you specified a message after the nickname, it '
                     'will immediately be sent to this user.',
                shortdesc='Query an user.',
137 138
                completion=self.completion_quoted)
        self.register_command('part', self.command_part,
139 140 141 142
                usage='[message]',
                desc='Disconnect from a room. You can'
                     ' specify an optional message.',
                shortdesc='Leave the room.')
143
        self.register_command('close', self.command_close,
144 145 146 147 148
                usage='[message]',
                desc='Disconnect from a room and close the tab.'
                     ' You can specify an optional message if '
                     'you are still connected.',
                shortdesc='Close the tab.')
149
        self.register_command('nick', self.command_nick,
150 151 152
                usage='<nickname>',
                desc='Change your nickname in the current room.',
                shortdesc='Change your nickname.',
153 154
                completion=self.completion_nick)
        self.register_command('recolor', self.command_recolor,
155 156 157 158 159 160 161
                usage='[random]',
                desc='Re-assign a color to all participants of the'
                     ' current room, based on the last time they talked.'
                     ' Use this if the participants currently talking '
                     'have too many identical colors. Use /recolor random'
                     ' for a non-deterministic result.',
                shortdesc='Change the nicks colors.',
162
                completion=self.completion_recolor)
163
        self.register_command('color', self.command_color,
164 165 166 167
                usage='<nick> <color>',
                desc='Fix a color for a nick. Use "unset" instead of a color'
                     ' to remove the attribution',
                shortdesc='Fix a color for a nick.',
168
                completion=self.completion_color)
169
        self.register_command('cycle', self.command_cycle,
170 171 172
                usage='[message]',
                desc='Leave the current room and rejoin it immediately.',
                shortdesc='Leave and re-join the room.')
173
        self.register_command('info', self.command_info,
174 175 176 177 178
                usage='<nickname>',
                desc='Display some information about the user '
                     'in the MUC: its/his/her role, affiliation,'
                     ' status and status message.',
                shortdesc='Show an user\'s infos.',
179 180
                completion=self.completion_info)
        self.register_command('configure', self.command_configure,
181 182
                desc='Configure the current room, through a form.',
                shortdesc='Configure the room.')
183
        self.register_command('version', self.command_version,
184 185 186 187 188
                usage='<jid or nick>',
                desc='Get the software version of the given JID'
                     ' or nick in room (usually its XMPP client'
                     ' and Operating System).',
                shortdesc='Get the software version of a jid.',
189 190
                completion=self.completion_version)
        self.register_command('names', self.command_names,
191 192
                desc='Get the users in the room with their roles.',
                shortdesc='List the users.')
193
        self.register_command('invite', self.command_invite,
194 195 196
                desc='Invite a contact to this room',
                usage='<jid> [reason]',
                shortdesc='Invite a contact to this room',
197 198 199 200 201 202 203 204
                completion=self.completion_invite)

        self.resize()
        self.update_commands()
        self.update_keys()

    @property
    def general_jid(self):
205
        return self.name
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234

    @property
    def is_muc(self):
        return True

    @property
    def last_connection(self):
        last_message = self._text_buffer.last_message
        if last_message:
            return last_message.time
        return None

    @refresh_wrapper.always
    def go_to_next_hl(self):
        """
        Go to the next HL in the room, or the last
        """
        self.text_win.next_highlight()

    @refresh_wrapper.always
    def go_to_prev_hl(self):
        """
        Go to the previous HL in the room, or the first
        """
        self.text_win.previous_highlight()

    def completion_version(self, the_input):
        """Completion for /version"""
        compare_users = lambda x: x.last_talked
235 236 237 238 239 240 241 242 243
        userlist = []
        for user in sorted(self.users, key=compare_users, reverse=True):
            if user.nick != self.own_nick:
                userlist.append(user.nick)
        comp = []
        for jid in (jid for jid in roster.jids() if len(roster[jid])):
            for resource in roster[jid].resources:
                comp.append(resource.jid)
        comp.sort()
244
        userlist.extend(comp)
245 246

        return the_input.auto_completion(userlist, quotify=False)
247 248 249 250

    def completion_info(self, the_input):
        """Completion for /info"""
        compare_users = lambda x: x.last_talked
251 252 253
        userlist = []
        for user in sorted(self.users, key=compare_users, reverse=True):
            userlist.append(user.nick)
254 255 256 257
        return the_input.auto_completion(userlist, quotify=False)

    def completion_nick(self, the_input):
        """Completion for /nick"""
258
        nicks = [os.environ.get('USER'),
259
                 config.get('default_nick'),
260
                 self.core.get_bookmark_nickname(self.name)]
261 262 263 264 265 266 267 268
        nicks = [i for i in nicks if i]
        return the_input.auto_completion(nicks, '', quotify=False)

    def completion_recolor(self, the_input):
        if the_input.get_argument_position() == 1:
            return the_input.new_completion(['random'], 1, '', quotify=False)
        return True

269 270 271 272 273 274 275 276 277 278 279
    def completion_color(self, the_input):
        """Completion for /color"""
        n = the_input.get_argument_position(quoted=True)
        if n == 1:
            userlist = [user.nick for user in self.users]
            if self.own_nick in userlist:
                userlist.remove(self.own_nick)
            return the_input.new_completion(userlist, 1, '', quotify=True)
        elif n == 2:
            colors = [i for i in xhtml.colors if i]
            colors.sort()
280
            colors.append('unset')
Eijebong's avatar
Eijebong committed
281
            colors.append('random')
282 283
            return the_input.new_completion(colors, 2, '', quotify=False)

284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
    def completion_ignore(self, the_input):
        """Completion for /ignore"""
        userlist = [user.nick for user in self.users]
        if self.own_nick in userlist:
            userlist.remove(self.own_nick)
        userlist.sort()
        return the_input.auto_completion(userlist, quotify=False)

    def completion_role(self, the_input):
        """Completion for /role"""
        n = the_input.get_argument_position(quoted=True)
        if n == 1:
            userlist = [user.nick for user in self.users]
            if self.own_nick in userlist:
                userlist.remove(self.own_nick)
            return the_input.new_completion(userlist, 1, '', quotify=True)
        elif n == 2:
            possible_roles = ['none', 'visitor', 'participant', 'moderator']
302 303
            return the_input.new_completion(possible_roles, 2, '',
                                            quotify=True)
304 305 306 307 308 309 310 311 312 313 314 315 316 317

    def completion_affiliation(self, the_input):
        """Completion for /affiliation"""
        n = the_input.get_argument_position(quoted=True)
        if n == 1:
            userlist = [user.nick for user in self.users]
            if self.own_nick in userlist:
                userlist.remove(self.own_nick)
            jidlist = [user.jid.bare for user in self.users]
            if self.core.xmpp.boundjid.bare in jidlist:
                jidlist.remove(self.core.xmpp.boundjid.bare)
            userlist.extend(jidlist)
            return the_input.new_completion(userlist, 1, '', quotify=True)
        elif n == 2:
318 319 320 321
            possible_affiliations = ['none', 'member', 'admin',
                                     'owner', 'outcast']
            return the_input.new_completion(possible_affiliations, 2, '',
                                            quotify=True)
322

323
    @command_args_parser.quoted(1, 1, [''])
324 325
    def command_invite(self, args):
        """/invite <jid> [reason]"""
326
        if args is None:
327
            return self.core.command.help('invite')
328
        jid, reason = args
329
        self.core.command.invite('%s %s "%s"' % (jid, self.name, reason))
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346

    def completion_invite(self, the_input):
        """Completion for /invite"""
        n = the_input.get_argument_position(quoted=True)
        if n == 1:
            return the_input.new_completion(roster.jids(), 1, quotify=True)

    def scroll_user_list_up(self):
        self.user_win.scroll_up()
        self.user_win.refresh(self.users)
        self.input.refresh()

    def scroll_user_list_down(self):
        self.user_win.scroll_down()
        self.user_win.refresh(self.users)
        self.input.refresh()

347 348
    @command_args_parser.quoted(1)
    def command_info(self, args):
349 350 351
        """
        /info <nick>
        """
352
        if args is None:
353
            return self.core.command.help('info')
354 355
        nick = args[0]
        user = self.get_user_by_name(nick)
356
        if not user:
mathieui's avatar
mathieui committed
357
            return self.core.information("Unknown user: %s" % nick, "Error")
358
        theme = get_theme()
mathieui's avatar
mathieui committed
359
        inf = '\x19' + dump_tuple(theme.COLOR_INFORMATION_TEXT) + '}'
360
        if user.jid:
mathieui's avatar
mathieui committed
361 362
            user_jid = '%s (\x19%s}%s\x19o%s)' % (
                            inf,
363
                            dump_tuple(theme.COLOR_MUC_JID),
mathieui's avatar
mathieui committed
364 365
                            user.jid,
                            inf)
366 367
        else:
            user_jid = ''
mathieui's avatar
mathieui committed
368 369
        info = ('\x19%s}%s\x19o%s%s: show: \x19%s}%s\x19o%s, affiliation:'
                ' \x19%s}%s\x19o%s, role: \x19%s}%s\x19o%s') % (
370
                        dump_tuple(user.color),
371
                        nick,
372
                        user_jid,
mathieui's avatar
mathieui committed
373
                        inf,
374 375
                        dump_tuple(theme.color_show(user.show)),
                        user.show or 'Available',
mathieui's avatar
mathieui committed
376
                        inf,
377 378
                        dump_tuple(theme.color_role(user.role)),
                        user.affiliation or 'None',
mathieui's avatar
mathieui committed
379
                        inf,
380 381 382
                        dump_tuple(theme.color_role(user.role)),
                        user.role or 'None',
                        '\n%s' % user.status if user.status else '')
383 384
        self.add_message(info, typ=0)
        self.core.refresh_window()
385

386 387
    @command_args_parser.quoted(0)
    def command_configure(self, ignored):
388 389 390
        """
        /configure
        """
391 392 393
        def on_form_received(form):
            if not form:
                self.core.information(
394 395
                    'Could not retrieve the configuration form',
                    'Error')
396 397 398
                return
            self.core.open_new_form(form, self.cancel_config, self.send_config)

399
        fixes.get_room_form(self.core.xmpp, self.name, on_form_received)
400 401 402 403 404

    def cancel_config(self, form):
        """
        The user do not want to send his/her config, send an iq cancel
        """
405
        muc.cancel_config(self.core.xmpp, self.name)
406 407 408 409 410 411
        self.core.close_tab()

    def send_config(self, form):
        """
        The user sends his/her config to the server
        """
412
        muc.configure_room(self.core.xmpp, self.name, form)
413 414
        self.core.close_tab()

415 416
    @command_args_parser.raw
    def command_cycle(self, msg):
417
        """/cycle [reason]"""
louiz’'s avatar
louiz’ committed
418
        self.command_part(msg)
419
        self.disconnect()
420
        self.user_win.pos = 0
421
        self.core.disable_private_tabs(self.name)
422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
        self.join()

    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:
            seconds = 0
        muc.join_groupchat(self.core, self.name, self.own_nick,
                           self.password,
                           status=status.message,
                           show=status.show,
                           seconds=seconds)
439

440 441
    @command_args_parser.quoted(0, 1, [''])
    def command_recolor(self, args):
442 443 444 445
        """
        /recolor [random]
        Re-assign color to the participants of the room
        """
446 447 448 449 450
        deterministic = config.get_by_tabname('deterministic_nick_colors', self.name)
        if deterministic:
            for user in self.users:
                if user.nick == self.own_nick:
                    continue
451
                color = self.search_for_color(user.nick)
452 453
                if color != '':
                    continue
454 455
                user.set_deterministic_color()
            if args[0] == 'random':
456 457 458
                self.core.information('"random" was provided, but poezio is '
                                      'configured to use deterministic colors',
                                      'Warning')
459 460 461
            self.user_win.refresh(self.users)
            self.input.refresh()
            return
462 463 464
        compare_users = lambda x: x.last_talked
        users = list(self.users)
        sorted_users = sorted(users, key=compare_users, reverse=True)
465
        full_sorted_users = sorted_users[:]
466
        # search our own user, to remove it from the list
467 468
        # Also remove users whose color is fixed
        for user in full_sorted_users:
469
            color = self.search_for_color(user.nick)
470 471 472
            if user.nick == self.own_nick:
                sorted_users.remove(user)
                user.color = get_theme().COLOR_OWN_NICK
473 474 475
            elif color != '':
                sorted_users.remove(user)
                user.change_color(color, deterministic)
476
        colors = list(get_theme().LIST_COLOR_NICKNAMES)
477
        if args[0] == 'random':
478 479 480 481 482 483 484 485
            random.shuffle(colors)
        for i, user in enumerate(sorted_users):
            user.color = colors[i % len(colors)]
        self.text_win.rebuild_everything(self._text_buffer)
        self.user_win.refresh(self.users)
        self.text_win.refresh()
        self.input.refresh()

486 487 488 489 490
    @command_args_parser.quoted(2, 2, [''])
    def command_color(self, args):
        """
        /color <nick> <color>
        Fix a color for a nick.
Eijebong's avatar
Eijebong committed
491 492
        Use "unset" instead of a color to remove the attribution.
        User "random" to attribute a random color.
493 494
        """
        if args is None:
495
            return self.core.command.help('color')
496 497 498
        nick = args[0]
        color = args[1].lower()
        user = self.get_user_by_name(nick)
Eijebong's avatar
Eijebong committed
499
        if not color in xhtml.colors and color not in ('unset', 'random'):
500
            return self.core.information("Unknown color: %s" % color, 'Error')
501
        if user and user.nick == self.own_nick:
502 503
            return self.core.information("You cannot change the color of your"
                                         " own nick.", 'Error')
504 505
        if color == 'unset':
            if config.remove_and_save(nick, 'muc_colors'):
506
                self.core.information('Color for nick %s unset' % (nick))
507
        else:
Eijebong's avatar
Eijebong committed
508 509
            if color == 'random':
                color = random.choice(list(xhtml.colors))
510 511
            if user:
                user.change_color(color)
512
            config.set_and_save(nick, color, 'muc_colors')
513 514 515 516
            nick_color_aliases = config.get_by_tabname('nick_color_aliases', self.name)
            if nick_color_aliases:
                # if any user in the room has a nick which is an alias of the
                # nick, update its color
517 518 519 520 521 522
                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)
523 524 525 526
            self.text_win.rebuild_everything(self._text_buffer)
            self.user_win.refresh(self.users)
            self.text_win.refresh()
            self.input.refresh()
527

528 529
    @command_args_parser.quoted(1)
    def command_version(self, args):
530 531 532 533 534
        """
        /version <jid or nick>
        """
        def callback(res):
            if not res:
535 536 537 538
                return self.core.information('Could not get the software '
                                             'version from %s' % (jid,),
                                             'Warning')
            version = '%s is running %s version %s on %s' % (
539
                         jid,
540 541 542
                         res.get('name') or 'an unknown software',
                         res.get('version') or 'unknown',
                         res.get('os') or 'an unknown platform')
543
            self.core.information(version, 'Info')
544
        if args is None:
545
            return self.core.command.help('version')
546 547
        nick = args[0]
        if nick in [user.nick for user in self.users]:
548
            jid = safeJID(self.name).bare
549
            jid = safeJID(jid + '/' + nick)
550
        else:
551
            jid = safeJID(nick)
552
        fixes.get_version(self.core.xmpp, jid,
553
                          callback=callback)
554

555 556
    @command_args_parser.quoted(1)
    def command_nick(self, args):
557 558 559
        """
        /nick <nickname>
        """
560
        if args is None:
561
            return self.core.command.help('nick')
562
        nick = args[0]
563
        if not self.joined:
564 565
            return self.core.information('/nick only works in joined rooms',
                                         'Info')
566
        current_status = self.core.get_status()
567
        if not safeJID(self.name + '/' + nick):
568
            return self.core.information('Invalid nick', 'Info')
569 570 571
        muc.change_nick(self.core, self.name, nick,
                        current_status.message,
                        current_status.show)
572

573 574
    @command_args_parser.quoted(0, 1, [''])
    def command_part(self, args):
575 576 577
        """
        /part [msg]
        """
578
        arg = args[0]
579 580
        msg = None
        if self.joined:
581 582 583 584
            info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
            char_quit = get_theme().CHAR_QUIT
            spec_col = dump_tuple(get_theme().COLOR_QUIT_CHAR)

585 586
            if config.get_by_tabname('display_user_color_in_join_part',
                                     self.general_jid):
587 588 589 590
                color = dump_tuple(get_theme().COLOR_OWN_NICK)
            else:
                color = 3

591
            if arg:
592 593 594 595 596 597 598 599 600
                msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
                       'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
                       ' left the chatroom'
                       ' (\x19o%(reason)s\x19%(info_col)s})') % {
                           'info_col': info_col, 'reason': arg,
                           'spec': char_quit, 'color': color,
                           'color_spec': spec_col,
                           'nick': self.own_nick,
                       }
601
            else:
602 603 604 605 606 607 608 609
                msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
                       'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
                       ' left the chatroom') % {
                           'info_col': info_col,
                           'spec': char_quit, 'color': color,
                           'color_spec': spec_col,
                           'nick': self.own_nick,
                       }
610

611
            self.add_message(msg, typ=2)
612 613
            self.disconnect()
            muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, arg)
614
            self.core.disable_private_tabs(self.name, reason=msg)
615 616 617
            if self == self.core.current_tab():
                self.refresh()
            self.core.doupdate()
618 619
        else:
            muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, arg)
620

621 622
    @command_args_parser.raw
    def command_close(self, msg):
623 624 625
        """
        /close [msg]
        """
louiz’'s avatar
louiz’ committed
626
        self.command_part(msg)
627 628
        self.core.close_tab()

629 630
    @command_args_parser.quoted(1, 1)
    def command_query(self, args):
631 632 633
        """
        /query <nick> [message]
        """
634
        if args is None:
635
            return  self.core.command.help('query')
636 637 638 639 640
        nick = args[0]
        r = None
        for user in self.users:
            if user.nick == nick:
                r = self.core.open_private_window(self.name, user.nick)
641
        if r and len(args) == 2:
642
            msg = args[1]
643 644
            self.core.current_tab().command_say(
                    xhtml.convert_simple_to_full_colors(msg))
645
        if not r:
646
            self.core.information("Cannot find user: %s" % nick, 'Error')
647

648 649
    @command_args_parser.raw
    def command_topic(self, subject):
650 651 652
        """
        /topic [new topic]
        """
653
        if not subject:
mathieui's avatar
mathieui committed
654 655 656 657 658 659 660 661 662 663 664 665 666
            info_text = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
            norm_text = dump_tuple(get_theme().COLOR_NORMAL_TEXT)
            if self.topic_from:
                user = self.get_user_by_name(self.topic_from)
                if user:
                    user_text = dump_tuple(user.color)
                    user_string = '\x19%s}(set by \x19%s}%s\x19%s})' % (
                            info_text, user_text, user.nick, info_text)
                else:
                    user_string = self.topic_from
            else:
                user_string = ''

667
            self._text_buffer.add_message(
mathieui's avatar
mathieui committed
668 669
                    "\x19%s}The subject of the room is: \x19%s}%s %s" %
                        (info_text, norm_text, self.topic, user_string))
670 671
            self.refresh()
            return
672

673 674
        muc.change_subject(self.core.xmpp, self.name, subject)

675 676
    @command_args_parser.quoted(0)
    def command_names(self, args):
677 678 679 680 681
        """
        /names
        """
        if not self.joined:
            return
682

683 684 685 686 687 688 689
        aff = {
                'owner': get_theme().CHAR_AFFILIATION_OWNER,
                'admin': get_theme().CHAR_AFFILIATION_ADMIN,
                'member': get_theme().CHAR_AFFILIATION_MEMBER,
                'none': get_theme().CHAR_AFFILIATION_NONE,
                }

690 691 692 693 694
        colors = {}
        colors["visitor"] = dump_tuple(get_theme().COLOR_USER_VISITOR)
        colors["moderator"] = dump_tuple(get_theme().COLOR_USER_MODERATOR)
        colors["participant"] = dump_tuple(get_theme().COLOR_USER_PARTICIPANT)
        color_other = dump_tuple(get_theme().COLOR_USER_NONE)
695 696

        buff = ['Users: %s \n' % len(self.users)]
697 698 699 700
        for user in self.users:
            affiliation = aff.get(user.affiliation,
                                  get_theme().CHAR_AFFILIATION_NONE)
            color = colors.get(user.role, color_other)
701
            buff.append('\x19%s}%s\x19o\x19%s}%s\x19o' % (
702 703
                color, affiliation, dump_tuple(user.color), user.nick))

704 705 706 707 708 709 710 711 712 713 714 715 716 717 718
        buff.append('\n')
        message = ' '.join(buff)

        self._text_buffer.add_message(message)
        self.text_win.refresh()
        self.input.refresh()

    def completion_topic(self, the_input):
        if the_input.get_argument_position() == 1:
            return the_input.auto_completion([self.topic], '', quotify=False)

    def completion_quoted(self, the_input):
        """Nick completion, but with quotes"""
        if the_input.get_argument_position(quoted=True) == 1:
            compare_users = lambda x: x.last_talked
719 720 721 722 723
            word_list = []
            for user in sorted(self.users, key=compare_users, reverse=True):
                if user.nick != self.own_nick:
                    word_list.append(user.nick)

724 725
            return the_input.new_completion(word_list, 1, quotify=True)

726 727
    @command_args_parser.quoted(1, 1)
    def command_kick(self, args):
728 729 730
        """
        /kick <nick> [reason]
        """
731
        if args is None:
732
            return self.core.command.help('kick')
733 734
        if len(args) == 2:
            msg = ' "%s"' % args[1]
735
        else:
736 737
            msg = ''
        self.command_role('"'+args[0]+ '" none'+msg)
738

739 740
    @command_args_parser.quoted(1, 1)
    def command_ban(self, args):
741 742 743 744 745
        """
        /ban <nick> [reason]
        """
        def callback(iq):
            if iq['type'] == 'error':
746
                self.core.room_error(iq, self.name)
747
        if args is None:
748
            return self.core.command.help('ban')
749 750 751 752 753 754 755
        if len(args) > 1:
            msg = args[1]
        else:
            msg = ''
        nick = args[0]

        if nick in [user.nick for user in self.users]:
756
            res = muc.set_user_affiliation(self.core.xmpp, self.name,
757 758
                                           'outcast', nick=nick,
                                           callback=callback, reason=msg)
759
        else:
760
            res = muc.set_user_affiliation(self.core.xmpp, self.name,
761 762
                                           'outcast', jid=safeJID(nick),
                                           callback=callback, reason=msg)
763 764 765
        if not res:
            self.core.information('Could not ban user', 'Error')

766 767
    @command_args_parser.quoted(2, 1, [''])
    def command_role(self, args):
768 769 770 771 772 773 774
        """
        /role <nick> <role> [reason]
        Changes the role of an user
        roles can be: none, visitor, participant, moderator
        """
        def callback(iq):
            if iq['type'] == 'error':
775
                self.core.room_error(iq, self.name)
776 777

        if args is None:
778
            return self.core.command.help('role')
779 780 781 782 783 784

        nick, role, reason = args[0], args[1].lower(), args[2]

        valid_roles = ('none', 'visitor', 'participant', 'moderator')

        if not self.joined or role not in valid_roles:
785 786
            return self.core.information('The role must be one of ' + ', '.join(valid_roles),
                                         'Error')
787

788
        if not safeJID(self.name + '/' + nick):
mathieui's avatar
mathieui committed
789
            return self.core.information('Invalid nick', 'Info')
790
        muc.set_user_role(self.core.xmpp, self.name, nick, reason, role,
791
                          callback=callback)
792

793 794
    @command_args_parser.quoted(2)
    def command_affiliation(self, args):
795 796 797 798 799 800 801
        """
        /affiliation <nick> <role>
        Changes the affiliation of an user
        affiliations can be: outcast, none, member, admin, owner
        """
        def callback(iq):
            if iq['type'] == 'error':
802
                self.core.room_error(iq, self.name)
803 804

        if args is None:
805
            return self.core.command.help('affiliation')
806

807
        nick, affiliation = args[0], args[1].lower()
808

809 810
        if not self.joined:
            return
811 812 813

        valid_affiliations = ('outcast', 'none', 'member', 'admin', 'owner')
        if affiliation not in valid_affiliations:
814 815
            return self.core.information('The affiliation must be one of ' + ', '.join(valid_affiliations),
                                         'Error')
816

817
        if nick in [user.nick for user in self.users]:
818
            res = muc.set_user_affiliation(self.core.xmpp, self.name,
819 820
                                           affiliation, nick=nick,
                                           callback=callback)
821
        else:
822
            res = muc.set_user_affiliation(self.core.xmpp, self.name,
823 824
                                           affiliation, jid=safeJID(nick),
                                           callback=callback)
825
        if not res:
826
            self.core.information('Could not set affiliation', 'Error')
827

828
    @command_args_parser.raw
829 830 831 832 833 834
    def command_say(self, line, correct=False):
        """
        /say <message>
        Or normal input + enter
        """
        needed = 'inactive' if self.inactive else 'active'
835
        msg = self.core.xmpp.make_message(self.name)
836 837 838 839 840 841 842 843 844 845 846 847 848 849 850
        msg['type'] = 'groupchat'
        msg['body'] = line
        # trigger the event BEFORE looking for colors.
        # This lets a plugin insert \x19xxx} colors, that will
        # be converted in xhtml.
        self.core.events.trigger('muc_say', msg, self)
        if not msg['body']:
            self.cancel_paused_delay()
            self.text_win.refresh()
            self.input.refresh()
            return
        if msg['body'].find('\x19') != -1:
            msg.enable('html')
            msg['html']['body'] = xhtml.poezio_colors_to_html(msg['body'])
            msg['body'] = xhtml.clean_text(msg['body'])
851 852
        if (config.get_by_tabname('send_chat_states', self.general_jid)
                and self.remote_wants_chatstates is not False):
853 854 855 856 857 858 859 860 861 862 863 864 865 866
            msg['chat_state'] = needed
        if correct:
            msg['replace']['id'] = self.last_sent_message['id']
        self.cancel_paused_delay()
        self.core.events.trigger('muc_say_after', msg, self)
        if not msg['body']:
            self.cancel_paused_delay()
            self.text_win.refresh()
            self.input.refresh()
            return
        self.last_sent_message = msg
        msg.send()
        self.chat_state = needed

867 868 869
    @command_args_parser.raw
    def command_xhtml(self, msg):
        message = self.generate_xhtml_message(msg)
870 871 872 873
        if message:
            message['type'] = 'groupchat'
            message.send()

874 875
    @command_args_parser.quoted(1)
    def command_ignore(self, args):
876 877 878
        """
        /ignore <nick>
        """
879
        if args is None:
880
            return self.core.command.help('ignore')
881 882

        nick = args[0]
883 884
        user = self.get_user_by_name(nick)
        if not user:
885
            self.core.information('%s is not in the room' % nick)
886
        elif user in self.ignores:
887
            self.core.information('%s is already ignored' % nick)
888 889
        else:
            self.ignores.append(user)
890
            self.core.information("%s is now ignored" % nick, 'info')
891

892 893
    @command_args_parser.quoted(1)
    def command_unignore(self, args):
894 895 896
        """
        /unignore <nick>
        """
897
        if args is None:
898
            return self.core.command.help('unignore')
899 900

        nick = args[0]
901 902
        user = self.get_user_by_name(nick)
        if not user:
903
            self.core.information('%s is not in the room' % nick)
904
        elif user not in self.ignores:
905
            self.core.information('%s is not ignored' % nick)
906 907
        else:
            self.ignores.remove(user)
908
            self.core.information('%s is now unignored' % nick)
909 910 911

    def completion_unignore(self, the_input):
        if the_input.get_argument_position() == 1:
912 913
            users = [user.nick for user in self.ignores]
            return the_input.auto_completion(users, quotify=False)
914 915 916 917 918

    def resize(self):
        """
        Resize the whole window. i.e. all its sub-windows
        """
919
        self.need_resize = False
920
        if config.get('hide_user_list') or self.size.tab_degrade_x:
921
            display_user_list = False
922 923
            text_width = self.width
        else:
924 925 926 927 928 929 930 931 932 933 934 935 936
            display_user_list = True
            text_width = (self.width // 10) * 9

        if self.size.tab_degrade_y:
            display_info_win = False
            tab_win_height = 0
            info_win_height = 0
        else:
            display_info_win = True
            tab_win_height = Tab.tab_win_height()
            info_win_height = self.core.information_win_size


937 938 939 940 941 942 943
        self.user_win.resize(self.height - 3 - info_win_height
                                - tab_win_height,
                             self.width - (self.width // 10) * 9 - 1,
                             1,
                             (self.width // 10) * 9 + 1)
        self.v_separator.resize(self.height - 3 - info_win_height - tab_win_height,
                                1, 1, 9 * (self.width // 10))
944

945
        self.topic_win.resize(1, self.width, 0, 0)
946 947 948 949

        self.text_win.resize(self.height - 3 - info_win_height
                                - tab_win_height,
                             text_width, 1, 0)
950
        self.text_win.rebuild_everything(self._text_buffer)
951
        self.info_header.resize(1, self.width,
952 953
                                self.height - 2 - info_win_height
                                    - tab_win_height,
954
                                0)
955 956 957 958 959
        self.input.resize(1, self.width, self.height-1, 0)

    def refresh(self):
        if self.need_resize:
            self.resize()
960
        log.debug('  TAB   Refresh: %s', self.__class__.__name__)
961
        if config.get('hide_user_list') or self.size.tab_degrade_x:
962 963 964
            display_user_list = False
        else:
            display_user_list = True
mathieui's avatar
mathieui committed
965
        display_info_win = not self.size.tab_degrade_y
966

967 968
        self.topic_win.refresh(self.get_single_line_topic())
        self.text_win.refresh()
969
        if display_user_list:
970 971
            self.v_separator.refresh()
            self.user_win.refresh(self.users)
972
        self.info_header.refresh(self, self.text_win, user=self.own_user)
973
        self.refresh_tab_win()
974 975
        if display_info_win:
            self.info_win.refresh()
976 977 978 979 980 981 982
        self.input.refresh()

    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)
983 984 985 986
        empty_after = self.input.get_text() == ''
        empty_after = empty_after or (self.input.get_text().startswith('/')
                                      and not
                                      self.input.get_text().startswith('//'))
987 988 989 990 991 992 993 994 995 996
        self.send_composing_chat_state(empty_after)
        return False

    def completion(self):
        """
        Called when Tab is pressed, complete the nickname in the input
        """
        if self.complete_commands(self.input):
            return

997 998
        # If we are not completing a command or a command argument,
        # complete a nick