muctab.py 71.7 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
from poezio.core.structs import Completion
36 37 38


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

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

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

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

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

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

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

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

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

270 271 272 273 274 275 276
    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)
277
            return Completion(the_input.new_completion, userlist, 1, '', quotify=True)
278 279 280
        elif n == 2:
            colors = [i for i in xhtml.colors if i]
            colors.sort()
281
            colors.append('unset')
Eijebong's avatar
Eijebong committed
282
            colors.append('random')
283
            return Completion(the_input.new_completion, colors, 2, '', quotify=False)
284

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

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

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

324
    @command_args_parser.quoted(1, 1, [''])
325 326
    def command_invite(self, args):
        """/invite <jid> [reason]"""
327
        if args is None:
328
            return self.core.command.help('invite')
329
        jid, reason = args
330
        self.core.command.invite('%s %s "%s"' % (jid, self.name, reason))
331 332 333 334 335

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


942 943 944 945 946 947 948
        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))
949

950
        self.topic_win.resize(1, self.width, 0, 0)
951 952 953 954

        self.text_win.resize(self.height - 3 - info_win_height
                                - tab_win_height,
                             text_width, 1, 0)
955
        self.text_win.rebuild_everything(self._text_buffer)
956
        self.info_header.resize(1, self.width,
957 958
                                self.height - 2 - info_win_height
                                    - tab_win_height,
959
                                0)
960 961 962 963 964
        self.input.resize(1, self.width, self.height-1, 0)

    def refresh(self):<