muctab.py 71.8 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 39 40 41 42 43 44 45 46 47 48


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

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

    @property
    def general_jid(self):
203
        return self.name
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232

    @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
233 234 235 236 237 238 239 240 241
        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()
242
        userlist.extend(comp)
243

244
        return Completion(the_input.auto_completion, userlist, quotify=False)
245 246 247 248

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

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

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

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

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

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

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

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

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

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

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

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

397
        fixes.get_room_form(self.core.xmpp, self.name, on_form_received)
398 399 400 401 402

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

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

413 414
    @command_args_parser.raw
    def command_cycle(self, msg):
415
        """/cycle [reason]"""
mathieui's avatar
mathieui committed
416
        self.leave_room(msg)
417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433
        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)
434

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

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

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

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

mathieui's avatar
mathieui committed
568
    def leave_room(self, message):
569
        if self.joined:
570 571 572 573
            info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
            char_quit = get_theme().CHAR_QUIT
            spec_col = dump_tuple(get_theme().COLOR_QUIT_CHAR)

574 575
            if config.get_by_tabname('display_user_color_in_join_part',
                                     self.general_jid):
576 577 578 579
                color = dump_tuple(get_theme().COLOR_OWN_NICK)
            else:
                color = 3

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

600
            self.add_message(msg, typ=2)
601
            self.disconnect()
mathieui's avatar
mathieui committed
602
            muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, message)
603
            self.core.disable_private_tabs(self.name, reason=msg)
604
        else:
mathieui's avatar
mathieui committed
605 606 607 608 609 610 611 612 613 614 615 616
            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()
617

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

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

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

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

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

674 675
        muc.change_subject(self.core.xmpp, self.name, subject)

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

684 685 686 687 688 689 690
        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,
                }

691 692 693 694 695
        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)
696 697

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

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

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

725
            return Completion(the_input.new_completion, word_list, 1, quotify=True)
726

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

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

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

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

        if args is None:
779
            return self.core.command.help('role')
780 781 782 783 784 785

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

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

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

        if args is None:
806
            return self.core.command.help('affiliation')
807

808
        nick, affiliation = args[0], args[1].lower()
809

810 811
        if not self.joined:
            return
812 813 814

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

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

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

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

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

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

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

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

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

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


934 935 936 937 938 939 940
        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))
941

942
        self.topic_win.resize(1, self.width, 0, 0)
943 944 945 946

        self.text_win.resize(self.height - 3 - info_win_height
                                - tab_win_height,
                             text_width, 1, 0)
947
        self.text_win.rebuild_everything(self._text_buffer)
948
        self.info_header.resize(1, self.width,
949 950
                                self.height - 2 - info_win_height
                                    - tab_win_height,
951
                                0)
952 953 954 955 956
        self.input.resize(1, self.width, self.height-1, 0)

    def refresh(self):
        if self.need_resize:
            self.resize()