muctab.py 72.4 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


NS_MUC_USER = 'http://jabber.org/protocol/muc#user'
39
STATUS_XPATH = '{%s}x/{%s}status' % (NS_MUC_USER, NS_MUC_USER)
40 41 42 43 44 45 46 47 48 49


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 = {}
50
    def __init__(self, core, jid, nick, password=None):
51
        self.joined = False
52
        ChatTab.__init__(self, core, jid)
53 54 55
        if self.joined == False:
            self._state = 'disconnected'
        self.own_nick = nick
56
        self.own_user = None
57
        self.name = jid
58
        self.password = password
59
        self.presence_buffer = []
60 61 62
        self.users = []
        self.privates = [] # private conversations
        self.topic = ''
63
        self.topic_from = ''
64
        self.remote_wants_chatstates = True
65 66
        # Self ping event, so we can cancel it when we leave the room
        self.self_ping_event = None
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
        # 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,
86 87 88
                usage='<nickname>',
                desc='Ignore a specified nickname.',
                shortdesc='Ignore someone',
89 90
                completion=self.completion_ignore)
        self.register_command('unignore', self.command_unignore,
91 92 93
                usage='<nickname>',
                desc='Remove the specified nickname from the ignore list.',
                shortdesc='Unignore someone.',
94 95
                completion=self.completion_unignore)
        self.register_command('kick', self.command_kick,
96 97 98 99
                usage='<nick> [reason]',
                desc='Kick the user with the specified nickname.'
                     ' You also can give an optional reason.',
                shortdesc='Kick someone.',
100 101
                completion=self.completion_quoted)
        self.register_command('ban', self.command_ban,
102 103 104
                usage='<nick> [reason]',
                desc='Ban the user with the specified nickname.'
                     ' You also can give an optional reason.',
105 106 107
                shortdesc='Ban someone',
                completion=self.completion_quoted)
        self.register_command('role', self.command_role,
108 109 110 111 112
                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.',
113 114
                completion=self.completion_role)
        self.register_command('affiliation', self.command_affiliation,
115 116 117 118
                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.',
119 120
                completion=self.completion_affiliation)
        self.register_command('topic', self.command_topic,
121
                usage='<subject>',
122 123 124 125 126
                desc='Change the subject of the room.',
                shortdesc='Change the subject.',
                completion=self.completion_topic)
        self.register_command('subject', self.command_topic,
                usage='<subject>',
127 128
                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 Completion(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
        return Completion(the_input.auto_completion, userlist, quotify=False)
255 256 257

    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
        nicks = [i for i in nicks if i]
262
        return Completion(the_input.auto_completion, nicks, '', quotify=False)
263 264 265

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

269 270 271 272 273 274 275
    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)
276
            return Completion(the_input.new_completion, userlist, 1, '', quotify=True)
277 278 279
        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
            return Completion(the_input.new_completion, colors, 2, '', quotify=False)
283

284 285 286 287 288 289
    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()
290
        return Completion(the_input.auto_completion, userlist, quotify=False)
291 292 293 294 295 296 297 298

    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)
299
            return Completion(the_input.new_completion, userlist, 1, '', quotify=True)
300 301
        elif n == 2:
            possible_roles = ['none', 'visitor', 'participant', 'moderator']
302
            return Completion(the_input.new_completion, possible_roles, 2, '',
303
                                            quotify=True)
304 305 306 307 308 309 310 311 312 313 314 315

    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)
316
            return Completion(the_input.new_completion, userlist, 1, '', quotify=True)
317
        elif n == 2:
318 319
            possible_affiliations = ['none', 'member', 'admin',
                                     'owner', 'outcast']
320
            return Completion(the_input.new_completion, possible_affiliations, 2, '',
321
                                            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

    def completion_invite(self, the_input):
        """Completion for /invite"""
        n = the_input.get_argument_position(quoted=True)
        if n == 1:
335
            return Completion(the_input.new_completion, roster.jids(), 1, quotify=True)
336 337 338 339 340 341 342 343 344 345 346

    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]"""
mathieui's avatar
mathieui committed
418
        self.leave_room(msg)
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
        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)
436

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

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

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

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

mathieui's avatar
mathieui committed
570
    def leave_room(self, message):
571
        self.presence_buffer = []
572
        if self.joined:
573 574 575 576
            info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
            char_quit = get_theme().CHAR_QUIT
            spec_col = dump_tuple(get_theme().COLOR_QUIT_CHAR)

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

mathieui's avatar
mathieui committed
583
            if message:
584 585
                msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
                       'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
586
                       ' left the room'
587
                       ' (\x19o%(reason)s\x19%(info_col)s})') % {
mathieui's avatar
mathieui committed
588
                           'info_col': info_col, 'reason': message,
589 590 591 592
                           'spec': char_quit, 'color': color,
                           'color_spec': spec_col,
                           'nick': self.own_nick,
                       }
593
            else:
594 595
                msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
                       'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
596
                       ' left the room') % {
597 598 599 600 601
                           'info_col': info_col,
                           'spec': char_quit, 'color': color,
                           'color_spec': spec_col,
                           'nick': self.own_nick,
                       }
602

603
            self.add_message(msg, typ=2)
604
            self.disconnect()
mathieui's avatar
mathieui committed
605
            muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, message)
606
            self.core.disable_private_tabs(self.name, reason=msg)
607
        else:
mathieui's avatar
mathieui committed
608 609 610 611 612 613 614 615 616 617 618 619
            muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, message)

    @command_args_parser.quoted(0, 1, [''])
    def command_part(self, args):
        """
        /part [msg]
        """
        message = args[0]
        self.leave_room(message)
        if self == self.core.current_tab():
            self.refresh()
        self.core.doupdate()
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 629 630 631
        self.core.close_tab(self)

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

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

652 653
    @command_args_parser.raw
    def command_topic(self, subject):
654 655 656
        """
        /topic [new topic]
        """
657
        if not subject:
mathieui's avatar
mathieui committed
658 659 660 661 662 663 664 665 666 667 668 669 670
            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 = ''

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

677 678
        muc.change_subject(self.core.xmpp, self.name, subject)

679 680
    @command_args_parser.quoted(0)
    def command_names(self, args):
681 682 683 684 685
        """
        /names
        """
        if not self.joined:
            return
686

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

694 695 696 697 698
        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)
699 700

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

708 709 710 711 712 713 714 715 716
        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:
717
            return Completion(the_input.auto_completion, [self.topic], '', quotify=False)
718 719 720 721 722

    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
723 724 725 726 727
            word_list = []
            for user in sorted(self.users, key=compare_users, reverse=True):
                if user.nick != self.own_nick:
                    word_list.append(user.nick)

728
            return Completion(the_input.new_completion, word_list, 1, quotify=True)
729

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

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

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

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

        if args is None:
782
            return self.core.command.help('role')
783 784 785 786 787 788

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

792
        if not safeJID(self.name + '/' + nick):
mathieui's avatar
mathieui committed
793
            return self.core.information('Invalid nick', 'Info')
794
        muc.set_user_role(self.core.xmpp, self.name, nick, reason, role,
795
                          callback=callback)
796

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

        if args is None:
809
            return self.core.command.help('affiliation')
810

811
        nick, affiliation = args[0], args[1].lower()
812

813 814
        if not self.joined:
            return
815 816 817

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

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

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

871 872 873
    @command_args_parser.raw
    def command_xhtml(self, msg):
        message = self.generate_xhtml_message(msg)
874 875 876 877
        if message:
            message['type'] = 'groupchat'
            message.send()

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

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

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

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

    def completion_unignore(self, the_input):
        if the_input.get_argument_position() == 1:
916
            users = [user.nick for user in self.ignores]
917
            return Completion(the_input.auto_completion, users, quotify=False)
918 919 920 921 922

    def resize(self):
        """
        Resize the whole window. i.e. all its sub-windows
        """
923
        self.need_resize = False
924
        if config.get('hide_user_list') or self.size.tab_degrade_x:
925 926
            text_width = self.width
        else:
927 928 929 930 931 932 933 934 935 936
            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


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