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

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

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

10 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

20
from poezio.tabs import ChatTab, Tab, SHOW_NAME
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
from poezio.core.structs import Completion, Status
36 37 38 39 40 41 42 43 44 45 46 47 48


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 = {}
49
    def __init__(self, core, jid, nick, password=None):
50
        self.joined = False
51
        ChatTab.__init__(self, core, jid)
52 53 54
        if self.joined == False:
            self._state = 'disconnected'
        self.own_nick = nick
55
        self.own_user = None
56
        self.name = jid
57
        self.password = password
58 59 60
        self.users = []
        self.privates = [] # private conversations
        self.topic = ''
61
        self.topic_from = ''
62
        self.remote_wants_chatstates = True
63 64
        # Self ping event, so we can cancel it when we leave the room
        self.self_ping_event = None
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
        # 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,
84 85 86
                usage='<nickname>',
                desc='Ignore a specified nickname.',
                shortdesc='Ignore someone',
87 88
                completion=self.completion_ignore)
        self.register_command('unignore', self.command_unignore,
89 90 91
                usage='<nickname>',
                desc='Remove the specified nickname from the ignore list.',
                shortdesc='Unignore someone.',
92 93
                completion=self.completion_unignore)
        self.register_command('kick', self.command_kick,
94 95 96 97
                usage='<nick> [reason]',
                desc='Kick the user with the specified nickname.'
                     ' You also can give an optional reason.',
                shortdesc='Kick someone.',
98 99
                completion=self.completion_quoted)
        self.register_command('ban', self.command_ban,
100 101 102
                usage='<nick> [reason]',
                desc='Ban the user with the specified nickname.'
                     ' You also can give an optional reason.',
103 104 105
                shortdesc='Ban someone',
                completion=self.completion_quoted)
        self.register_command('role', self.command_role,
106 107 108 109 110
                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.',
111 112
                completion=self.completion_role)
        self.register_command('affiliation', self.command_affiliation,
113 114 115 116
                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.',
117 118
                completion=self.completion_affiliation)
        self.register_command('topic', self.command_topic,
119 120 121
                usage='<subject>',
                desc='Change the subject of the room.',
                shortdesc='Change the subject.',
122 123
                completion=self.completion_topic)
        self.register_command('query', self.command_query,
124 125 126 127 128 129
                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.',
130 131
                completion=self.completion_quoted)
        self.register_command('part', self.command_part,
132 133 134 135
                usage='[message]',
                desc='Disconnect from a room. You can'
                     ' specify an optional message.',
                shortdesc='Leave the room.')
136
        self.register_command('close', self.command_close,
137 138 139 140 141
                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.')
142
        self.register_command('nick', self.command_nick,
143 144 145
                usage='<nickname>',
                desc='Change your nickname in the current room.',
                shortdesc='Change your nickname.',
146 147
                completion=self.completion_nick)
        self.register_command('recolor', self.command_recolor,
148 149 150 151 152 153 154
                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.',
155
                completion=self.completion_recolor)
156
        self.register_command('color', self.command_color,
157 158 159 160
                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.',
161
                completion=self.completion_color)
162
        self.register_command('cycle', self.command_cycle,
163 164 165
                usage='[message]',
                desc='Leave the current room and rejoin it immediately.',
                shortdesc='Leave and re-join the room.')
166
        self.register_command('info', self.command_info,
167 168 169 170 171
                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.',
172 173
                completion=self.completion_info)
        self.register_command('configure', self.command_configure,
174 175
                desc='Configure the current room, through a form.',
                shortdesc='Configure the room.')
176
        self.register_command('version', self.command_version,
177 178 179 180 181
                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.',
182 183
                completion=self.completion_version)
        self.register_command('names', self.command_names,
184 185
                desc='Get the users in the room with their roles.',
                shortdesc='List the users.')
186
        self.register_command('invite', self.command_invite,
187 188 189
                desc='Invite a contact to this room',
                usage='<jid> [reason]',
                shortdesc='Invite a contact to this room',
190 191 192 193 194 195 196 197
                completion=self.completion_invite)

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

    @property
    def general_jid(self):
198
        return self.name
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227

    @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
228 229 230 231 232 233 234 235 236
        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()
237
        userlist.extend(comp)
238

239
        return Completion(the_input.auto_completion, userlist, quotify=False)
240 241 242 243

    def completion_info(self, the_input):
        """Completion for /info"""
        compare_users = lambda x: x.last_talked
244 245 246
        userlist = []
        for user in sorted(self.users, key=compare_users, reverse=True):
            userlist.append(user.nick)
247
        return Completion(the_input.auto_completion, userlist, quotify=False)
248 249 250

    def completion_nick(self, the_input):
        """Completion for /nick"""
251
        nicks = [os.environ.get('USER'),
252
                 config.get('default_nick'),
253
                 self.core.get_bookmark_nickname(self.name)]
254
        nicks = [i for i in nicks if i]
255
        return Completion(the_input.auto_completion, nicks, '', quotify=False)
256 257 258

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

262 263 264 265 266 267 268
    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)
269
            return Completion(the_input.new_completion, userlist, 1, '', quotify=True)
270 271 272
        elif n == 2:
            colors = [i for i in xhtml.colors if i]
            colors.sort()
273
            colors.append('unset')
Eijebong's avatar
Eijebong committed
274
            colors.append('random')
275
            return Completion(the_input.new_completion, colors, 2, '', quotify=False)
276

277 278 279 280 281 282
    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()
283
        return Completion(the_input.auto_completion, userlist, quotify=False)
284 285 286 287 288 289 290 291

    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)
292
            return Completion(the_input.new_completion, userlist, 1, '', quotify=True)
293 294
        elif n == 2:
            possible_roles = ['none', 'visitor', 'participant', 'moderator']
295
            return Completion(the_input.new_completion, possible_roles, 2, '',
296
                                            quotify=True)
297 298 299 300 301 302 303 304 305 306 307 308

    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)
309
            return Completion(the_input.new_completion, userlist, 1, '', quotify=True)
310
        elif n == 2:
311 312
            possible_affiliations = ['none', 'member', 'admin',
                                     'owner', 'outcast']
313
            return Completion(the_input.new_completion, possible_affiliations, 2, '',
314
                                            quotify=True)
315

316
    @command_args_parser.quoted(1, 1, [''])
317 318
    def command_invite(self, args):
        """/invite <jid> [reason]"""
319
        if args is None:
320
            return self.core.command.help('invite')
321
        jid, reason = args
322
        self.core.command.invite('%s %s "%s"' % (jid, self.name, reason))
323 324 325 326 327

    def completion_invite(self, the_input):
        """Completion for /invite"""
        n = the_input.get_argument_position(quoted=True)
        if n == 1:
328
            return Completion(the_input.new_completion, roster.jids(), 1, quotify=True)
329 330 331 332 333 334 335 336 337 338 339

    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()

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

379 380
    @command_args_parser.quoted(0)
    def command_configure(self, ignored):
381 382 383
        """
        /configure
        """
384 385 386
        def on_form_received(form):
            if not form:
                self.core.information(
387 388
                    'Could not retrieve the configuration form',
                    'Error')
389 390 391
                return
            self.core.open_new_form(form, self.cancel_config, self.send_config)

392
        fixes.get_room_form(self.core.xmpp, self.name, on_form_received)
393 394 395 396 397

    def cancel_config(self, form):
        """
        The user do not want to send his/her config, send an iq cancel
        """
398
        muc.cancel_config(self.core.xmpp, self.name)
399 400 401 402 403 404
        self.core.close_tab()

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

408 409
    @command_args_parser.raw
    def command_cycle(self, msg):
410
        """/cycle [reason]"""
louiz’'s avatar
louiz’ committed
411
        self.command_part(msg)
412
        self.disconnect()
413
        self.user_win.pos = 0
414
        self.core.disable_private_tabs(self.name)
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431
        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)
432

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

479 480 481 482 483
    @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
484 485
        Use "unset" instead of a color to remove the attribution.
        User "random" to attribute a random color.
486 487
        """
        if args is None:
488
            return self.core.command.help('color')
489 490 491
        nick = args[0]
        color = args[1].lower()
        user = self.get_user_by_name(nick)
Eijebong's avatar
Eijebong committed
492
        if not color in xhtml.colors and color not in ('unset', 'random'):
493
            return self.core.information("Unknown color: %s" % color, 'Error')
494
        if user and user.nick == self.own_nick:
495 496
            return self.core.information("You cannot change the color of your"
                                         " own nick.", 'Error')
497 498
        if color == 'unset':
            if config.remove_and_save(nick, 'muc_colors'):
499
                self.core.information('Color for nick %s unset' % (nick))
500
        else:
Eijebong's avatar
Eijebong committed
501 502
            if color == 'random':
                color = random.choice(list(xhtml.colors))
503 504
            if user:
                user.change_color(color)
505
            config.set_and_save(nick, color, 'muc_colors')
506 507 508 509
            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
510 511 512 513 514 515
                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)
516 517 518 519
            self.text_win.rebuild_everything(self._text_buffer)
            self.user_win.refresh(self.users)
            self.text_win.refresh()
            self.input.refresh()
520

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

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

566 567
    @command_args_parser.quoted(0, 1, [''])
    def command_part(self, args):
568 569 570
        """
        /part [msg]
        """
571
        arg = args[0]
572 573
        msg = None
        if self.joined:
574 575 576 577
            info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
            char_quit = get_theme().CHAR_QUIT
            spec_col = dump_tuple(get_theme().COLOR_QUIT_CHAR)

578 579
            if config.get_by_tabname('display_user_color_in_join_part',
                                     self.general_jid):
580 581 582 583
                color = dump_tuple(get_theme().COLOR_OWN_NICK)
            else:
                color = 3

584
            if arg:
585 586
                msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
                       'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
587
                       ' left the room'
588 589 590 591 592 593
                       ' (\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,
                       }
594
            else:
595 596
                msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
                       'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
597
                       ' left the room') % {
598 599 600 601 602
                           'info_col': info_col,
                           'spec': char_quit, 'color': color,
                           'color_spec': spec_col,
                           'nick': self.own_nick,
                       }
603

604
            self.add_message(msg, typ=2)
605 606
            self.disconnect()
            muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, arg)
607
            self.core.disable_private_tabs(self.name, reason=msg)
608 609 610
            if self == self.core.current_tab():
                self.refresh()
            self.core.doupdate()
611 612
        else:
            muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, arg)
613

614 615
    @command_args_parser.raw
    def command_close(self, msg):
616 617 618
        """
        /close [msg]
        """
louiz’'s avatar
louiz’ committed
619
        self.command_part(msg)
620 621 622 623 624
        self.core.close_tab(self)

    def on_close(self):
        super().on_close()
        self.command_part('')
625

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

645 646
    @command_args_parser.raw
    def command_topic(self, subject):
647 648 649
        """
        /topic [new topic]
        """
650
        if not subject:
mathieui's avatar
mathieui committed
651 652 653 654 655 656 657 658 659 660 661 662 663
            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 = ''

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

670 671
        muc.change_subject(self.core.xmpp, self.name, subject)

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

680 681 682 683 684 685 686
        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,
                }

687 688 689 690 691
        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)
692 693

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

701 702 703 704 705 706 707 708 709
        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:
710
            return Completion(the_input.auto_completion, [self.topic], '', quotify=False)
711 712 713 714 715

    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
716 717 718 719 720
            word_list = []
            for user in sorted(self.users, key=compare_users, reverse=True):
                if user.nick != self.own_nick:
                    word_list.append(user.nick)

721
            return Completion(the_input.new_completion, word_list, 1, quotify=True)
722

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

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

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

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

        if args is None:
775
            return self.core.command.help('role')
776 777 778 779 780 781

        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:
782 783
            return self.core.information('The role must be one of ' + ', '.join(valid_roles),
                                         'Error')
784

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

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

        if args is None:
802
            return self.core.command.help('affiliation')
803

804
        nick, affiliation = args[0], args[1].lower()
805

806 807
        if not self.joined:
            return
808 809 810

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

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

825
    @command_args_parser.raw
826 827 828 829 830 831
    def command_say(self, line, correct=False):
        """
        /say <message>
        Or normal input + enter
        """
        needed = 'inactive' if self.inactive else 'active'
832
        msg = self.core.xmpp.make_message(self.name)
833 834 835 836 837 838 839 840 841 842 843 844 845 846 847
        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'])
848 849
        if (config.get_by_tabname('send_chat_states', self.general_jid)
                and self.remote_wants_chatstates is not False):
850 851 852 853 854 855 856 857 858 859 860 861 862 863
            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

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

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

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

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

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

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

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

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


930 931 932 933 934 935 936
        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))
937

938
        self.topic_win.resize(1, self.width, 0, 0)
939 940 941 942

        self.text_win.resize(self.height - 3 - info_win_height
                                - tab_win_height,
                             text_width, 1, 0)
943
        self.text_win.rebuild_everything(self._text_buffer)
944
        self.info_header.resize(1, self.width,
945 946
                                self.height - 2 - info_win_height
                                    - tab_win_height,
947
                                0)
948 949 950 951 952
        self.input.resize(1, self.width, self.height-1, 0)

    def refresh(self):
        if self.need_resize:
            self.resize()
953
        log.debug('  TAB   Refresh: %s', self.__class__.__name__)
mathieui's avatar