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

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

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

10 11 12
import logging
log = logging.getLogger(__name__)

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

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

22 23 24 25 26 27 28 29 30 31 32 33 34
from poezio import common
from poezio import fixes
from poezio import multiuserchat as muc
from poezio import timed_events
from poezio import windows
from poezio import xhtml
from poezio.common import safeJID
from poezio.config import config
from poezio.decorators import refresh_wrapper, command_args_parser
from poezio.logger import logger
from poezio.roster import roster
from poezio.theming import get_theme, dump_tuple
from poezio.user import User
35 36 37


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

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


class MucTab(ChatTab):
    """
    The tab containing a multi-user-chat room.
    It contains an userlist, an input, a topic, an information and a chat zone
    """
    message_type = 'groupchat'
    plugin_commands = {}
    plugin_keys = {}
56
    def __init__(self, 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
            if self == self.core.current_tab():
                self.refresh()
            self.core.doupdate()
620 621
        else:
            muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, arg)
622

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

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

650 651
    @command_args_parser.raw
    def command_topic(self, subject):
652 653 654
        """
        /topic [new topic]
        """
655
        if not subject:
656
            self._text_buffer.add_message(
657
                    "\x19%s}The subject of the room is: %s %s" %
658
                        (dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
659 660 661
                         self.topic,
                         '(set by %s)' % self.topic_from if self.topic_from
                                                         else ''))
662 663
            self.refresh()
            return
664

665 666
        muc.change_subject(self.core.xmpp, self.name, subject)

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

675 676 677 678 679 680 681
        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,
                }

682 683 684 685 686
        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)
687 688

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

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

716 717
            return the_input.new_completion(word_list, 1, quotify=True)

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

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

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

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

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

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

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

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

        if args is None:
797
            return self.core.command.help('affiliation')
798

799
        nick, affiliation = args[0], args[1].lower()
800

801 802
        if not self.joined:
            return
803 804 805

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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