muctab.py 70.6 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 21 22 23 24 25 26 27 28 29

from . import ChatTab, Tab

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

        if self.core.xmpp.boundjid.server == "gmail.com": #gmail sucks
            del self.commands["nick"]

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

    @property
    def general_jid(self):
207
        return self.name
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 235 236

    @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
237 238 239 240 241 242 243 244 245
        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()
246
        userlist.extend(comp)
247 248

        return the_input.auto_completion(userlist, quotify=False)
249 250 251 252

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

    def completion_nick(self, the_input):
        """Completion for /nick"""
260
        nicks = [os.environ.get('USER'),
261
                 config.get('default_nick'),
262
                 self.core.get_bookmark_nickname(self.name)]
263 264 265 266 267 268 269 270
        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

271 272 273 274 275 276 277 278 279 280 281
    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()
282
            colors.append('unset')
Eijebong's avatar
Eijebong committed
283
            colors.append('random')
284 285
            return the_input.new_completion(colors, 2, '', quotify=False)

286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
    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']
304 305
            return the_input.new_completion(possible_roles, 2, '',
                                            quotify=True)
306 307 308 309 310 311 312 313 314 315 316 317 318 319

    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:
320 321 322 323
            possible_affiliations = ['none', 'member', 'admin',
                                     'owner', 'outcast']
            return the_input.new_completion(possible_affiliations, 2, '',
                                            quotify=True)
324

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

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

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

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

401
        fixes.get_room_form(self.core.xmpp, self.name, on_form_received)
402 403 404 405 406

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

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

417 418
    @command_args_parser.raw
    def command_cycle(self, msg):
419
        """/cycle [reason]"""
louiz’'s avatar
louiz’ committed
420
        self.command_part(msg)
421
        self.disconnect()
422
        self.user_win.pos = 0
423
        self.core.disable_private_tabs(self.name)
424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
        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)
441

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

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

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

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

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

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

593
            if arg:
594 595 596 597 598 599 600 601 602
                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,
                       }
603
            else:
604 605 606 607 608 609 610 611
                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,
                       }
612

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

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:
654
            self._text_buffer.add_message(
655
                    "\x19%s}The subject of the room is: %s %s" %
656
                        (dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
657 658 659
                         self.topic,
                         '(set by %s)' % self.topic_from if self.topic_from
                                                         else ''))
660 661
            self.refresh()
            return
662

663 664
        muc.change_subject(self.core.xmpp, self.name, subject)

665 666
    @command_args_parser.quoted(0)
    def command_names(self, args):
667 668 669 670 671
        """
        /names
        """
        if not self.joined:
            return
672

673 674 675 676 677 678 679
        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,
                }

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

        buff = ['Users: %s \n' % len(self.users)]
687 688 689 690
        for user in self.users:
            affiliation = aff.get(user.affiliation,
                                  get_theme().CHAR_AFFILIATION_NONE)
            color = colors.get(user.role, color_other)
691
            buff.append('\x19%s}%s\x19o\x19%s}%s\x19o' % (
692 693
                color, affiliation, dump_tuple(user.color), user.nick))

694 695 696 697 698 699 700 701 702 703 704 705 706 707 708
        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
709 710 711 712 713
            word_list = []
            for user in sorted(self.users, key=compare_users, reverse=True):
                if user.nick != self.own_nick:
                    word_list.append(user.nick)

714 715
            return the_input.new_completion(word_list, 1, quotify=True)

716 717
    @command_args_parser.quoted(1, 1)
    def command_kick(self, args):
718 719 720
        """
        /kick <nick> [reason]
        """
721
        if args is None:
722
            return self.core.command.help('kick')
723 724
        if len(args) == 2:
            msg = ' "%s"' % args[1]
725
        else:
726 727
            msg = ''
        self.command_role('"'+args[0]+ '" none'+msg)
728

729 730
    @command_args_parser.quoted(1, 1)
    def command_ban(self, args):
731 732 733 734 735
        """
        /ban <nick> [reason]
        """
        def callback(iq):
            if iq['type'] == 'error':
736
                self.core.room_error(iq, self.name)
737
        if args is None:
738
            return self.core.command.help('ban')
739 740 741 742 743 744 745
        if len(args) > 1:
            msg = args[1]
        else:
            msg = ''
        nick = args[0]

        if nick in [user.nick for user in self.users]:
746
            res = muc.set_user_affiliation(self.core.xmpp, self.name,
747 748
                                           'outcast', nick=nick,
                                           callback=callback, reason=msg)
749
        else:
750
            res = muc.set_user_affiliation(self.core.xmpp, self.name,
751 752
                                           'outcast', jid=safeJID(nick),
                                           callback=callback, reason=msg)
753 754 755
        if not res:
            self.core.information('Could not ban user', 'Error')

756 757
    @command_args_parser.quoted(2, 1, [''])
    def command_role(self, args):
758 759 760 761 762 763 764
        """
        /role <nick> <role> [reason]
        Changes the role of an user
        roles can be: none, visitor, participant, moderator
        """
        def callback(iq):
            if iq['type'] == 'error':
765
                self.core.room_error(iq, self.name)
766 767

        if args is None:
768
            return self.core.command.help('role')
769 770 771 772 773 774

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

778
        if not safeJID(self.name + '/' + nick):
mathieui's avatar
mathieui committed
779
            return self.core.information('Invalid nick', 'Info')
780
        muc.set_user_role(self.core.xmpp, self.name, nick, reason, role,
781
                          callback=callback)
782

783 784
    @command_args_parser.quoted(2)
    def command_affiliation(self, args):
785 786 787 788 789 790 791
        """
        /affiliation <nick> <role>
        Changes the affiliation of an user
        affiliations can be: outcast, none, member, admin, owner
        """
        def callback(iq):
            if iq['type'] == 'error':
792
                self.core.room_error(iq, self.name)
793 794

        if args is None:
795
            return self.core.command.help('affiliation')
796

797
        nick, affiliation = args[0], args[1].lower()
798

799 800
        if not self.joined:
            return
801 802 803

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

807
        if nick in [user.nick for user in self.users]:
808
            res = muc.set_user_affiliation(self.core.xmpp, self.name,
809 810
                                           affiliation, nick=nick,
                                           callback=callback)
811
        else:
812
            res = muc.set_user_affiliation(self.core.xmpp, self.name,
813 814
                                           affiliation, jid=safeJID(nick),
                                           callback=callback)
815
        if not res:
816
            self.core.information('Could not set affiliation', 'Error')
817

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

857 858 859
    @command_args_parser.raw
    def command_xhtml(self, msg):
        message = self.generate_xhtml_message(msg)
860 861 862 863
        if message:
            message['type'] = 'groupchat'
            message.send()

864 865
    @command_args_parser.quoted(1)
    def command_ignore(self, args):
866 867 868
        """
        /ignore <nick>
        """
869
        if args is None:
870
            return self.core.command.help('ignore')
871 872

        nick = args[0]
873 874
        user = self.get_user_by_name(nick)
        if not user:
875
            self.core.information('%s is not in the room' % nick)
876
        elif user in self.ignores:
877
            self.core.information('%s is already ignored' % nick)
878 879
        else:
            self.ignores.append(user)
880
            self.core.information("%s is now ignored" % nick, 'info')
881

882 883
    @command_args_parser.quoted(1)
    def command_unignore(self, args):
884 885 886
        """
        /unignore <nick>
        """
887
        if args is None:
888
            return self.core.command.help('unignore')
889 890

        nick = args[0]
891 892
        user = self.get_user_by_name(nick)
        if not user:
893
            self.core.information('%s is not in the room' % nick)
894
        elif user not in self.ignores:
895
            self.core.information('%s is not ignored' % nick)
896 897
        else:
            self.ignores.remove(user)
898
            self.core.information('%s is now unignored' % nick)
899 900 901

    def completion_unignore(self, the_input):
        if the_input.get_argument_position() == 1:
902 903
            users = [user.nick for user in self.ignores]
            return the_input.auto_completion(users, quotify=False)
904 905 906 907 908

    def resize(self):
        """
        Resize the whole window. i.e. all its sub-windows
        """
909
        self.need_resize = False
910
        if config.get('hide_user_list') or self.size.tab_degrade_x:
911
            display_user_list = False
912 913
            text_width = self.width
        else:
914 915 916 917 918 919 920 921 922 923 924 925 926
            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


927 928 929 930 931 932 933
        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))
934

935
        self.topic_win.resize(1, self.width, 0, 0)
936 937 938 939

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

    def refresh(self):
        if self.need_resize:
            self.resize()
950
        log.debug('  TAB   Refresh: %s', self.__class__.__name__)
951
        if config.get('hide_user_list') or self.size.tab_degrade_x:
952 953 954
            display_user_list = False
        else:
            display_user_list = True
mathieui's avatar
mathieui committed
955
        display_info_win = not self.size.tab_degrade_y
956

957 958
        self.topic_win.refresh(self.get_single_line_topic())
        self.text_win.refresh()
959
        if display_user_list:
960 961 962 963
            self.v_separator.refresh()
            self.user_win.refresh(self.users)
        self.info_header.refresh(self, self.text_win)
        self.refresh_tab_win()
964 965
        if display_info_win:
            self.info_win.refresh()
966 967 968 969 970 971 972
        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)
973 974 975 976
        empty_after = self.input.get_text() == ''
        empty_after = empty_after or (self.input.get_text().startswith('/')
                                      and not
                                      self.input.get_text().startswith('//'))
977 978 979 980 981 982 983 984 985 986
        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

987 988
        # If we are not completing a command or a command argument,
        # complete a nick
989
        compare_users = lambda x: x.last_talked
990 991 992 993
        word_list = []
        for user in sorted(self.users, key=compare_users, reverse=True):
            if user.nick != self.own_nick:
                word_list.append(user.nick)
994
        after = config.get('after_completion') + ' '
995
        input_pos = self.input.pos
996 997 998 999
        if ' ' not in self.input.get_text()[:input_pos] or (
                self.input.last_completion and
                    self.input.get_text()[:input_pos] ==
                    self.input.last_completion + after):
1000 1001
            add_after = after
        else: