muctab.py 74 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

51 52
    def __init__(self, core, jid, nick, password=None):
        ChatTab.__init__(self, core, jid)
53 54 55
        self.joined = False
        self._state = 'disconnected'
        # our nick in the MUC
56
        self.own_nick = nick
57
        # self User object
58
        self.own_user = None
59
        self.name = jid
60
        self.password = password
61
        # buffered presences
62
        self.presence_buffer = []
63
        # userlist
64
        self.users = []
65 66
        # private conversations
        self.privates = []
67
        self.topic = ''
68
        self.topic_from = ''
69 70 71
        # 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.
72 73 74 75
        self.remote_wants_chatstates = True
        # Self ping event, so we can cancel it when we leave the room
        self.self_ping_event = None
        # UI stuff
76 77 78 79 80 81 82
        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()
83 84
        # List of ignored users
        self.ignores = []
85
        # keys
86 87
        self.register_keys()
        self.update_keys()
88
        # commands
89
        self.register_commands()
90
        self.update_commands()
91
        self.resize()
92 93 94

    @property
    def general_jid(self):
95
        return self.name
96

mathieui's avatar
mathieui committed
97 98 99 100
    def check_send_chat_state(self):
        "If we should send a chat state"
        return self.joined

101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
    @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
125 126 127 128 129 130 131 132 133
        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()
134
        userlist.extend(comp)
135

136
        return Completion(the_input.auto_completion, userlist, quotify=False)
137 138 139 140

    def completion_info(self, the_input):
        """Completion for /info"""
        compare_users = lambda x: x.last_talked
141 142 143
        userlist = []
        for user in sorted(self.users, key=compare_users, reverse=True):
            userlist.append(user.nick)
144
        return Completion(the_input.auto_completion, userlist, quotify=False)
145 146 147

    def completion_nick(self, the_input):
        """Completion for /nick"""
148
        nicks = [os.environ.get('USER'),
149
                 config.get('default_nick'),
150
                 self.core.get_bookmark_nickname(self.name)]
151
        nicks = [i for i in nicks if i]
152
        return Completion(the_input.auto_completion, nicks, '', quotify=False)
153 154 155

    def completion_recolor(self, the_input):
        if the_input.get_argument_position() == 1:
156
            return Completion(the_input.new_completion, ['random'], 1, '', quotify=False)
157 158
        return True

159 160 161 162 163 164 165
    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)
166
            return Completion(the_input.new_completion, userlist, 1, '', quotify=True)
167 168 169
        elif n == 2:
            colors = [i for i in xhtml.colors if i]
            colors.sort()
170
            colors.append('unset')
Eijebong's avatar
Eijebong committed
171
            colors.append('random')
172
            return Completion(the_input.new_completion, colors, 2, '', quotify=False)
173

174 175 176 177 178 179
    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()
180
        return Completion(the_input.auto_completion, userlist, quotify=False)
181 182 183 184 185 186 187 188

    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)
189
            return Completion(the_input.new_completion, userlist, 1, '', quotify=True)
190 191
        elif n == 2:
            possible_roles = ['none', 'visitor', 'participant', 'moderator']
192
            return Completion(the_input.new_completion, possible_roles, 2, '',
193
                                            quotify=True)
194 195 196 197 198 199 200 201 202 203 204 205

    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)
206
            return Completion(the_input.new_completion, userlist, 1, '', quotify=True)
207
        elif n == 2:
208 209
            possible_affiliations = ['none', 'member', 'admin',
                                     'owner', 'outcast']
210
            return Completion(the_input.new_completion, possible_affiliations, 2, '',
211
                                            quotify=True)
212

213
    @command_args_parser.quoted(1, 1, [''])
214 215
    def command_invite(self, args):
        """/invite <jid> [reason]"""
216
        if args is None:
217
            return self.core.command.help('invite')
218
        jid, reason = args
219
        self.core.command.invite('%s %s "%s"' % (jid, self.name, reason))
220 221 222 223 224

    def completion_invite(self, the_input):
        """Completion for /invite"""
        n = the_input.get_argument_position(quoted=True)
        if n == 1:
225
            return Completion(the_input.new_completion, roster.jids(), 1, quotify=True)
226 227 228 229 230 231 232 233 234 235 236

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

237 238
    @command_args_parser.quoted(1)
    def command_info(self, args):
239 240 241
        """
        /info <nick>
        """
242
        if args is None:
243
            return self.core.command.help('info')
244 245
        nick = args[0]
        user = self.get_user_by_name(nick)
246
        if not user:
mathieui's avatar
mathieui committed
247
            return self.core.information("Unknown user: %s" % nick, "Error")
248
        theme = get_theme()
mathieui's avatar
mathieui committed
249
        inf = '\x19' + dump_tuple(theme.COLOR_INFORMATION_TEXT) + '}'
250
        if user.jid:
mathieui's avatar
mathieui committed
251 252
            user_jid = '%s (\x19%s}%s\x19o%s)' % (
                            inf,
253
                            dump_tuple(theme.COLOR_MUC_JID),
mathieui's avatar
mathieui committed
254 255
                            user.jid,
                            inf)
256 257
        else:
            user_jid = ''
mathieui's avatar
mathieui committed
258 259
        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') % (
260
                        dump_tuple(user.color),
261
                        nick,
262
                        user_jid,
mathieui's avatar
mathieui committed
263
                        inf,
264 265
                        dump_tuple(theme.color_show(user.show)),
                        user.show or 'Available',
mathieui's avatar
mathieui committed
266
                        inf,
267 268
                        dump_tuple(theme.color_role(user.role)),
                        user.affiliation or 'None',
mathieui's avatar
mathieui committed
269
                        inf,
270 271 272
                        dump_tuple(theme.color_role(user.role)),
                        user.role or 'None',
                        '\n%s' % user.status if user.status else '')
273 274
        self.add_message(info, typ=0)
        self.core.refresh_window()
275

276 277
    @command_args_parser.quoted(0)
    def command_configure(self, ignored):
278 279 280
        """
        /configure
        """
281 282 283
        def on_form_received(form):
            if not form:
                self.core.information(
284 285
                    'Could not retrieve the configuration form',
                    'Error')
286 287 288
                return
            self.core.open_new_form(form, self.cancel_config, self.send_config)

289
        fixes.get_room_form(self.core.xmpp, self.name, on_form_received)
290 291 292 293 294

    def cancel_config(self, form):
        """
        The user do not want to send his/her config, send an iq cancel
        """
295
        muc.cancel_config(self.core.xmpp, self.name)
296 297 298 299 300 301
        self.core.close_tab()

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

305 306
    @command_args_parser.raw
    def command_cycle(self, msg):
307
        """/cycle [reason]"""
mathieui's avatar
mathieui committed
308
        self.leave_room(msg)
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
        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)
326

327 328
    @command_args_parser.quoted(0, 1, [''])
    def command_recolor(self, args):
329 330 331 332
        """
        /recolor [random]
        Re-assign color to the participants of the room
        """
333 334 335 336 337
        deterministic = config.get_by_tabname('deterministic_nick_colors', self.name)
        if deterministic:
            for user in self.users:
                if user.nick == self.own_nick:
                    continue
338
                color = self.search_for_color(user.nick)
339 340
                if color != '':
                    continue
341 342
                user.set_deterministic_color()
            if args[0] == 'random':
343 344 345
                self.core.information('"random" was provided, but poezio is '
                                      'configured to use deterministic colors',
                                      'Warning')
346 347 348
            self.user_win.refresh(self.users)
            self.input.refresh()
            return
349 350 351
        compare_users = lambda x: x.last_talked
        users = list(self.users)
        sorted_users = sorted(users, key=compare_users, reverse=True)
352
        full_sorted_users = sorted_users[:]
353
        # search our own user, to remove it from the list
354 355
        # Also remove users whose color is fixed
        for user in full_sorted_users:
356
            color = self.search_for_color(user.nick)
357 358 359
            if user.nick == self.own_nick:
                sorted_users.remove(user)
                user.color = get_theme().COLOR_OWN_NICK
360 361 362
            elif color != '':
                sorted_users.remove(user)
                user.change_color(color, deterministic)
363
        colors = list(get_theme().LIST_COLOR_NICKNAMES)
364
        if args[0] == 'random':
365 366 367 368 369 370 371 372
            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()

373 374 375 376 377
    @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
378 379
        Use "unset" instead of a color to remove the attribution.
        User "random" to attribute a random color.
380 381
        """
        if args is None:
382
            return self.core.command.help('color')
383 384 385
        nick = args[0]
        color = args[1].lower()
        user = self.get_user_by_name(nick)
386
        if color not in xhtml.colors and color not in ('unset', 'random'):
387
            return self.core.information("Unknown color: %s" % color, 'Error')
388
        if user and user.nick == self.own_nick:
389 390
            return self.core.information("You cannot change the color of your"
                                         " own nick.", 'Error')
391 392
        if color == 'unset':
            if config.remove_and_save(nick, 'muc_colors'):
393
                self.core.information('Color for nick %s unset' % (nick))
394
        else:
Eijebong's avatar
Eijebong committed
395 396
            if color == 'random':
                color = random.choice(list(xhtml.colors))
397 398
            if user:
                user.change_color(color)
399
            config.set_and_save(nick, color, 'muc_colors')
400 401 402 403
            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
404 405 406 407 408 409
                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)
410 411 412 413
            self.text_win.rebuild_everything(self._text_buffer)
            self.user_win.refresh(self.users)
            self.text_win.refresh()
            self.input.refresh()
414

415 416
    @command_args_parser.quoted(1)
    def command_version(self, args):
417 418 419 420 421
        """
        /version <jid or nick>
        """
        def callback(res):
            if not res:
422 423 424 425
                return self.core.information('Could not get the software '
                                             'version from %s' % (jid,),
                                             'Warning')
            version = '%s is running %s version %s on %s' % (
426
                         jid,
427 428 429
                         res.get('name') or 'an unknown software',
                         res.get('version') or 'unknown',
                         res.get('os') or 'an unknown platform')
430
            self.core.information(version, 'Info')
431
        if args is None:
432
            return self.core.command.help('version')
433 434
        nick = args[0]
        if nick in [user.nick for user in self.users]:
435
            jid = safeJID(self.name).bare
436
            jid = safeJID(jid + '/' + nick)
437
        else:
438
            jid = safeJID(nick)
439
        fixes.get_version(self.core.xmpp, jid,
440
                          callback=callback)
441

442 443
    @command_args_parser.quoted(1)
    def command_nick(self, args):
444 445 446
        """
        /nick <nickname>
        """
447
        if args is None:
448
            return self.core.command.help('nick')
449
        nick = args[0]
450
        if not self.joined:
451 452
            return self.core.information('/nick only works in joined rooms',
                                         'Info')
453
        current_status = self.core.get_status()
454
        if not safeJID(self.name + '/' + nick):
455
            return self.core.information('Invalid nick', 'Info')
456 457 458
        muc.change_nick(self.core, self.name, nick,
                        current_status.message,
                        current_status.show)
459

mathieui's avatar
mathieui committed
460
    def leave_room(self, message):
461
        if self.joined:
462 463 464 465
            info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
            char_quit = get_theme().CHAR_QUIT
            spec_col = dump_tuple(get_theme().COLOR_QUIT_CHAR)

466 467
            if config.get_by_tabname('display_user_color_in_join_part',
                                     self.general_jid):
468 469 470 471
                color = dump_tuple(get_theme().COLOR_OWN_NICK)
            else:
                color = 3

mathieui's avatar
mathieui committed
472
            if message:
473 474
                msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
                       'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
475
                       ' left the room'
476
                       ' (\x19o%(reason)s\x19%(info_col)s})') % {
mathieui's avatar
mathieui committed
477
                           'info_col': info_col, 'reason': message,
478 479 480 481
                           'spec': char_quit, 'color': color,
                           'color_spec': spec_col,
                           'nick': self.own_nick,
                       }
482
            else:
483 484
                msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
                       'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
485
                       ' left the room') % {
486 487 488 489 490
                           'info_col': info_col,
                           'spec': char_quit, 'color': color,
                           'color_spec': spec_col,
                           'nick': self.own_nick,
                       }
491

492
            self.add_message(msg, typ=2)
493
            self.disconnect()
mathieui's avatar
mathieui committed
494
            muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, message)
495
            self.core.disable_private_tabs(self.name, reason=msg)
496
        else:
mathieui's avatar
mathieui committed
497 498 499 500 501 502 503 504 505 506 507 508
            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()
509

510 511
    @command_args_parser.raw
    def command_close(self, msg):
512 513 514
        """
        /close [msg]
        """
louiz’'s avatar
louiz’ committed
515
        self.command_part(msg)
516 517 518 519 520
        self.core.close_tab(self)

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

522 523
    @command_args_parser.quoted(1, 1)
    def command_query(self, args):
524 525 526
        """
        /query <nick> [message]
        """
527
        if args is None:
528
            return  self.core.command.help('query')
529 530 531 532 533
        nick = args[0]
        r = None
        for user in self.users:
            if user.nick == nick:
                r = self.core.open_private_window(self.name, user.nick)
534
        if r and len(args) == 2:
535
            msg = args[1]
536 537
            self.core.current_tab().command_say(
                    xhtml.convert_simple_to_full_colors(msg))
538
        if not r:
539
            self.core.information("Cannot find user: %s" % nick, 'Error')
540

541 542
    @command_args_parser.raw
    def command_topic(self, subject):
543 544 545
        """
        /topic [new topic]
        """
546
        if not subject:
mathieui's avatar
mathieui committed
547 548 549 550 551 552 553 554 555 556 557 558 559
            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 = ''

560
            self._text_buffer.add_message(
mathieui's avatar
mathieui committed
561 562
                    "\x19%s}The subject of the room is: \x19%s}%s %s" %
                        (info_text, norm_text, self.topic, user_string))
563 564
            self.refresh()
            return
565

566 567
        muc.change_subject(self.core.xmpp, self.name, subject)

568 569
    @command_args_parser.quoted(0)
    def command_names(self, args):
570 571 572 573 574
        """
        /names
        """
        if not self.joined:
            return
575

576 577 578 579 580 581 582
        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,
                }

583 584 585 586 587
        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)
588 589

        buff = ['Users: %s \n' % len(self.users)]
590 591 592 593
        for user in self.users:
            affiliation = aff.get(user.affiliation,
                                  get_theme().CHAR_AFFILIATION_NONE)
            color = colors.get(user.role, color_other)
594
            buff.append('\x19%s}%s\x19o\x19%s}%s\x19o' % (
595 596
                color, affiliation, dump_tuple(user.color), user.nick))

597 598 599 600 601 602 603 604 605
        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:
606
            return Completion(the_input.auto_completion, [self.topic], '', quotify=False)
607 608 609 610 611

    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
612 613 614 615 616
            word_list = []
            for user in sorted(self.users, key=compare_users, reverse=True):
                if user.nick != self.own_nick:
                    word_list.append(user.nick)

617
            return Completion(the_input.new_completion, word_list, 1, quotify=True)
618

619 620
    @command_args_parser.quoted(1, 1)
    def command_kick(self, args):
621 622 623
        """
        /kick <nick> [reason]
        """
624
        if args is None:
625
            return self.core.command.help('kick')
626 627
        if len(args) == 2:
            msg = ' "%s"' % args[1]
628
        else:
629 630
            msg = ''
        self.command_role('"'+args[0]+ '" none'+msg)
631

632 633
    @command_args_parser.quoted(1, 1)
    def command_ban(self, args):
634 635 636 637 638
        """
        /ban <nick> [reason]
        """
        def callback(iq):
            if iq['type'] == 'error':
639
                self.core.room_error(iq, self.name)
640
        if args is None:
641
            return self.core.command.help('ban')
642 643 644 645 646 647 648
        if len(args) > 1:
            msg = args[1]
        else:
            msg = ''
        nick = args[0]

        if nick in [user.nick for user in self.users]:
649
            res = muc.set_user_affiliation(self.core.xmpp, self.name,
650 651
                                           'outcast', nick=nick,
                                           callback=callback, reason=msg)
652
        else:
653
            res = muc.set_user_affiliation(self.core.xmpp, self.name,
654 655
                                           'outcast', jid=safeJID(nick),
                                           callback=callback, reason=msg)
656 657 658
        if not res:
            self.core.information('Could not ban user', 'Error')

659 660
    @command_args_parser.quoted(2, 1, [''])
    def command_role(self, args):
661 662 663 664 665 666 667
        """
        /role <nick> <role> [reason]
        Changes the role of an user
        roles can be: none, visitor, participant, moderator
        """
        def callback(iq):
            if iq['type'] == 'error':
668
                self.core.room_error(iq, self.name)
669 670

        if args is None:
671
            return self.core.command.help('role')
672 673 674 675 676 677

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

681
        if not safeJID(self.name + '/' + nick):
mathieui's avatar
mathieui committed
682
            return self.core.information('Invalid nick', 'Info')
683
        muc.set_user_role(self.core.xmpp, self.name, nick, reason, role,
684
                          callback=callback)
685

686 687
    @command_args_parser.quoted(2)
    def command_affiliation(self, args):
688 689 690 691 692 693 694
        """
        /affiliation <nick> <role>
        Changes the affiliation of an user
        affiliations can be: outcast, none, member, admin, owner
        """
        def callback(iq):
            if iq['type'] == 'error':
695
                self.core.room_error(iq, self.name)
696 697

        if args is None:
698
            return self.core.command.help('affiliation')
699

700
        nick, affiliation = args[0], args[1].lower()
701

702 703
        if not self.joined:
            return
704 705 706

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

710
        if nick in [user.nick for user in self.users]:
711
            res = muc.set_user_affiliation(self.core.xmpp, self.name,
712 713
                                           affiliation, nick=nick,
                                           callback=callback)
714
        else:
715
            res = muc.set_user_affiliation(self.core.xmpp, self.name,
716 717
                                           affiliation, jid=safeJID(nick),
                                           callback=callback)
718
        if not res:
719
            self.core.information('Could not set affiliation', 'Error')
720

721
    @command_args_parser.raw
722 723 724 725 726 727
    def command_say(self, line, correct=False):
        """
        /say <message>
        Or normal input + enter
        """
        needed = 'inactive' if self.inactive else 'active'
728
        msg = self.core.xmpp.make_message(self.name)
729 730 731 732 733 734 735 736 737 738 739 740 741 742 743
        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'])
744 745
        if (config.get_by_tabname('send_chat_states', self.general_jid)
                and self.remote_wants_chatstates is not False):
746 747 748 749 750 751 752 753 754 755 756 757 758 759
            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

760 761 762
    @command_args_parser.raw
    def command_xhtml(self, msg):
        message = self.generate_xhtml_message(msg)
763 764 765 766
        if message:
            message['type'] = 'groupchat'
            message.send()

767 768
    @command_args_parser.quoted(1)
    def command_ignore(self, args):
769 770 771
        """
        /ignore <nick>
        """
772
        if args is None:
773
            return self.core.command.help('ignore')
774 775

        nick = args[0]
776 777
        user = self.get_user_by_name(nick)
        if not user:
778
            self.core.information('%s is not in the room' % nick)
779
        elif user in self.ignores:
780
            self.core.information('%s is already ignored' % nick)
781 782
        else:
            self.ignores.append(user)
783
            self.core.information("%s is now ignored" % nick, 'info')
784

785 786
    @command_args_parser.quoted(1)
    def command_unignore(self, args):
787 788 789
        """
        /unignore <nick>
        """
790
        if args is None:
791
            return self.core.command.help('unignore')
792 793

        nick = args[0]
794 795
        user = self.get_user_by_name(nick)
        if not user:
796
            self.core.information('%s is not in the room' % nick)
797
        elif user not in self.ignores:
798
            self.core.information('%s is not ignored' % nick)
799 800
        else:
            self.ignores.remove(user)
801
            self.core.information('%s is now unignored' % nick)
802 803 804

    def completion_unignore(self, the_input):
        if the_input.get_argument_position() == 1:
805
            users = [user.nick for user in self.ignores]
806
            return Completion(the_input.auto_completion, users, quotify=False)
807 808 809 810 811

    def resize(self):
        """
        Resize the whole window. i.e. all its sub-windows
        """
812
        self.need_resize = False
813
        if config.get('hide_user_list') or self.size.tab_degrade_x:
814 815
            text_width = self.width
        else:
816 817 818 819 820 821 822 823 824 825
            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


826 827 828 829 830 831 832
        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))
833

834
        self.topic_win.resize(1, self.width, 0, 0)
835 836 837 838

        self.text_win.resize(self.height - 3 - info_win_height
                                - tab_win_height,
                             text_width, 1, 0)
839
        self.text_win.rebuild_everything(self._text_buffer)
840
        self.info_header.resize(1, self.width,
841 842
                                self.height - 2 - info_win_height
                                    - tab_win_height,
843
                                0)
844 845 846 847 848
        self.input.resize(1, self.width, self.height-1, 0)

    def refresh(self):
        if self.need_resize:
            self.resize()
849
        log.debug('  TAB   Refresh: %s', self.__class__.__name__)
850
        if config.get('hide_user_list') or self.size.tab_degrade_x:
851 852 853
            display_user_list = False
        else:
            display_user_list = True
mathieui's avatar
mathieui committed
854
        display_info_win = not self.size.tab_degrade_y
855

856 857
        self.topic_win.refresh(self.get_single_line_topic())
        self.text_win.refresh()
858
        if display_user_list:
859 860
            self.v_separator.refresh()
            self.user_win.refresh(self.users)
861
        self.info_header.refresh(self, self.text_win, user=self.own_user)
862
        self.refresh_tab_win()
863 864
        if display_info_win:
            self.info_win.refresh()
865 866 867 868 869 870 871
        self.input.refresh()

    def on_input(self, key, raw):
        if not raw and key in self.key_func:
            self.key_func[key]()
            return False
        self.input.do_command(key, raw=raw)
872 873 874 875
        empty_after = self.input.get_text() == ''
        empty_after = empty_after or (self.input.get_text().startswith('/')
                                      and not
                                      self.input.get_text().startswith('//'))
876 877 878 879 880 881 882 883 884 885
        self.send_composing_chat_state(empty_after)
        return False

    def completion(self):
        """
        Called when Tab is pressed, complete the nickname in the input
        """
        if self.complete_commands(self.input):
            return

886 887
        # If we are not completing a command or a command argument,
        # complete a nick
888
        compare_users = lambda x: x.last_talked
889 890 891 892
        word_list = []
        for user in sorted(self.users, key=compare_users, reverse=True):
            if user.nick != self.own_nick:
                word_list.append(user.nick)
893
        after = config.get('after_completion') + ' '
894
        input_pos = self.input.pos
895 896 897 898
        if ' ' not in self.input.get_text()[:input_pos] or (
                self.input.last_completion and
                    self.input.get_text()[:input_pos] ==
                    self.input.last_completion + after):
899 900
            add_after = after
        else:
901
            if not config.get('add_space_after_completion'):
902 903 904
                add_after = ''
            else:
                add_after = ' '
905
        self.input.auto_completion(word_list, add_after, quotify=False)
906 907 908 909
        empty_after = self.input.get_text() == ''
        empty_after = empty_after or (self.input.get_text().startswith('/')
                                      and not
                                      self.input.get_text().startswith('//'))
910 911 912
        self.send_composing_chat_state(empty_after)

    def get_nick(self):
913
        if not config.get('show_muc_jid'):
914 915 916 917 918 919 920 921
            return safeJID(self.name).user
        return self.name

    def get_text_window(self):
        return self.text_win

    def on_lose_focus(self):
        if self.joined:
922 923 924 925
            if self.input.text:
                self.state = 'nonempty'
            else:
                self.state = 'normal'
926 927 928 929
        else:
            self.state = 'disconnected'
        self.text_win.remove_line_separator()
        self.text_win.add_line_separator(self._text_buffer)
930
        if config.get_by_tabname('send_chat_states', self.general_jid):
931 932 933 934 935
            self.send_chat_state('inactive')
        self.check_scrolled()

    def on_gain_focus(self):
        self.state = 'current'
936
        if (self.text_win.built_lines and self.text_win.built_lines[-1] is