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

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

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

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

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

20
from poezio.tabs import ChatTab, Tab, SHOW_NAME
21

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


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


class MucTab(ChatTab):
    """
    The tab containing a multi-user-chat room.
    It contains an userlist, an input, a topic, an information and a chat zone
    """
    message_type = 'groupchat'
    plugin_commands = {}
    plugin_keys = {}
50
    def __init__(self, core, jid, nick, password=None):
51
        self.joined = False
52
        ChatTab.__init__(self, core, jid)
53 54 55
        if self.joined == False:
            self._state = 'disconnected'
        self.own_nick = nick
56
        self.own_user = None
57
        self.name = jid
58
        self.password = password
59
        self.presence_buffer = []
60 61 62
        self.users = []
        self.privates = [] # private conversations
        self.topic = ''
63
        self.topic_from = ''
64
        self.remote_wants_chatstates = True
65 66
        # Self ping event, so we can cancel it when we leave the room
        self.self_ping_event = None
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
        # We send active, composing and paused states to the MUC because
        # the chatstate may or may not be filtered by the MUC,
        # that’s not our problem.
        self.topic_win = windows.Topic()
        self.text_win = windows.TextWin()
        self._text_buffer.add_window(self.text_win)
        self.v_separator = windows.VerticalSeparator()
        self.user_win = windows.UserList()
        self.info_header = windows.MucInfoWin()
        self.input = windows.MessageInput()
        self.ignores = []       # set of Users
        # keys
        self.key_func['^I'] = self.completion
        self.key_func['M-u'] = self.scroll_user_list_down
        self.key_func['M-y'] = self.scroll_user_list_up
        self.key_func['M-n'] = self.go_to_next_hl
        self.key_func['M-p'] = self.go_to_prev_hl
        # commands
        self.register_command('ignore', self.command_ignore,
86 87 88
                usage='<nickname>',
                desc='Ignore a specified nickname.',
                shortdesc='Ignore someone',
89 90
                completion=self.completion_ignore)
        self.register_command('unignore', self.command_unignore,
91 92 93
                usage='<nickname>',
                desc='Remove the specified nickname from the ignore list.',
                shortdesc='Unignore someone.',
94 95
                completion=self.completion_unignore)
        self.register_command('kick', self.command_kick,
96 97 98 99
                usage='<nick> [reason]',
                desc='Kick the user with the specified nickname.'
                     ' You also can give an optional reason.',
                shortdesc='Kick someone.',
100 101
                completion=self.completion_quoted)
        self.register_command('ban', self.command_ban,
102 103 104
                usage='<nick> [reason]',
                desc='Ban the user with the specified nickname.'
                     ' You also can give an optional reason.',
105 106 107
                shortdesc='Ban someone',
                completion=self.completion_quoted)
        self.register_command('role', self.command_role,
108 109 110 111 112
                usage='<nick> <role> [reason]',
                desc='Set the role of an user. Roles can be:'
                     ' none, visitor, participant, moderator.'
                     ' You also can give an optional reason.',
                shortdesc='Set the role of an user.',
113 114
                completion=self.completion_role)
        self.register_command('affiliation', self.command_affiliation,
115 116 117 118
                usage='<nick or jid> <affiliation>',
                desc='Set the affiliation of an user. Affiliations can be:'
                     ' outcast, none, member, admin, owner.',
                shortdesc='Set the affiliation of an user.',
119 120
                completion=self.completion_affiliation)
        self.register_command('topic', self.command_topic,
121
                usage='<subject>',
122 123 124 125 126
                desc='Change the subject of the room.',
                shortdesc='Change the subject.',
                completion=self.completion_topic)
        self.register_command('subject', self.command_topic,
                usage='<subject>',
127 128
                desc='Change the subject of the room.',
                shortdesc='Change the subject.',
129 130
                completion=self.completion_topic)
        self.register_command('query', self.command_query,
131 132 133 134 135 136
                usage='<nick> [message]',
                desc='Open a private conversation with <nick>. This nick'
                     ' has to be present in the room you\'re currently in.'
                     ' If you specified a message after the nickname, it '
                     'will immediately be sent to this user.',
                shortdesc='Query an user.',
137 138
                completion=self.completion_quoted)
        self.register_command('part', self.command_part,
139 140 141 142
                usage='[message]',
                desc='Disconnect from a room. You can'
                     ' specify an optional message.',
                shortdesc='Leave the room.')
143
        self.register_command('close', self.command_close,
144 145 146 147 148
                usage='[message]',
                desc='Disconnect from a room and close the tab.'
                     ' You can specify an optional message if '
                     'you are still connected.',
                shortdesc='Close the tab.')
149
        self.register_command('nick', self.command_nick,
150 151 152
                usage='<nickname>',
                desc='Change your nickname in the current room.',
                shortdesc='Change your nickname.',
153 154
                completion=self.completion_nick)
        self.register_command('recolor', self.command_recolor,
155 156 157 158 159 160 161
                usage='[random]',
                desc='Re-assign a color to all participants of the'
                     ' current room, based on the last time they talked.'
                     ' Use this if the participants currently talking '
                     'have too many identical colors. Use /recolor random'
                     ' for a non-deterministic result.',
                shortdesc='Change the nicks colors.',
162
                completion=self.completion_recolor)
163
        self.register_command('color', self.command_color,
164 165 166 167
                usage='<nick> <color>',
                desc='Fix a color for a nick. Use "unset" instead of a color'
                     ' to remove the attribution',
                shortdesc='Fix a color for a nick.',
168
                completion=self.completion_color)
169
        self.register_command('cycle', self.command_cycle,
170 171 172
                usage='[message]',
                desc='Leave the current room and rejoin it immediately.',
                shortdesc='Leave and re-join the room.')
173
        self.register_command('info', self.command_info,
174 175 176 177 178
                usage='<nickname>',
                desc='Display some information about the user '
                     'in the MUC: its/his/her role, affiliation,'
                     ' status and status message.',
                shortdesc='Show an user\'s infos.',
179 180
                completion=self.completion_info)
        self.register_command('configure', self.command_configure,
181 182
                desc='Configure the current room, through a form.',
                shortdesc='Configure the room.')
183
        self.register_command('version', self.command_version,
184 185 186 187 188
                usage='<jid or nick>',
                desc='Get the software version of the given JID'
                     ' or nick in room (usually its XMPP client'
                     ' and Operating System).',
                shortdesc='Get the software version of a jid.',
189 190
                completion=self.completion_version)
        self.register_command('names', self.command_names,
191 192
                desc='Get the users in the room with their roles.',
                shortdesc='List the users.')
193
        self.register_command('invite', self.command_invite,
194 195 196
                desc='Invite a contact to this room',
                usage='<jid> [reason]',
                shortdesc='Invite a contact to this room',
197 198 199 200 201 202 203 204
                completion=self.completion_invite)

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

    @property
    def general_jid(self):
205
        return self.name
206 207 208 209 210

    @property
    def is_muc(self):
        return True

mathieui's avatar
mathieui committed
211 212 213 214
    def check_send_chat_state(self):
        "If we should send a chat state"
        return self.joined

215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
    @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
239 240 241 242 243 244 245 246 247
        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()
248
        userlist.extend(comp)
249

250
        return Completion(the_input.auto_completion, userlist, quotify=False)
251 252 253 254

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

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

    def completion_recolor(self, the_input):
        if the_input.get_argument_position() == 1:
270
            return Completion(the_input.new_completion, ['random'], 1, '', quotify=False)
271 272
        return True

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

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

    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)
303
            return Completion(the_input.new_completion, userlist, 1, '', quotify=True)
304 305
        elif n == 2:
            possible_roles = ['none', 'visitor', 'participant', 'moderator']
306
            return Completion(the_input.new_completion, possible_roles, 2, '',
307
                                            quotify=True)
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)
320
            return Completion(the_input.new_completion, userlist, 1, '', quotify=True)
321
        elif n == 2:
322 323
            possible_affiliations = ['none', 'member', 'admin',
                                     'owner', 'outcast']
324
            return Completion(the_input.new_completion, possible_affiliations, 2, '',
325
                                            quotify=True)
326

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

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

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

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

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

403
        fixes.get_room_form(self.core.xmpp, self.name, on_form_received)
404 405 406 407 408

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

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

419 420
    @command_args_parser.raw
    def command_cycle(self, msg):
421
        """/cycle [reason]"""
mathieui's avatar
mathieui committed
422
        self.leave_room(msg)
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)
500
        if color not 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

mathieui's avatar
mathieui committed
574
    def leave_room(self, message):
575
        if self.joined:
576 577 578 579
            info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
            char_quit = get_theme().CHAR_QUIT
            spec_col = dump_tuple(get_theme().COLOR_QUIT_CHAR)

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

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

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

    @command_args_parser.quoted(0, 1, [''])
    def command_part(self, args):
        """
        /part [msg]
        """
        message = args[0]
        self.leave_room(message)
        if self == self.core.current_tab():
            self.refresh()
        self.core.doupdate()
623

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

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

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

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

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

680 681
        muc.change_subject(self.core.xmpp, self.name, subject)

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

690 691 692 693 694 695 696
        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,
                }

697 698 699 700 701
        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)
702 703

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

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

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

731
            return Completion(the_input.new_completion, word_list, 1, quotify=True)
732

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

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

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

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

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

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

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

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

        if args is None:
812
            return self.core.command.help('affiliation')
813

814
        nick, affiliation = args[0], args[1].lower()
815

816 817
        if not self.joined:
            return
818 819 820

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

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

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

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

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

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

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

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

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

    def resize(self):
        """
        Resize the whole window. i.e. all its sub-windows
        """
926
        self.need_resize = False
927
        if config.get('hide_user_list') or self.size.tab_degrade_x:
928 929
            text_width = self.width
        else:
930 931 932 933 934 935 936 937 938 939
            text_width = (self.width // 10) * 9

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


940 941 942 943 944 945 946
        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))
947

948
        self.topic_win.resize(1, self.width, 0, 0)
949 950 951 952

        self.text_win.resize(self.height - 3 - info_win_height
                                - tab_win_height,
                             text_width, 1, 0)
953
        self.text_win.rebuild_everything(self._text_buffer)