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

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

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

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

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

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

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


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

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


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

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

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

    @property
    def is_muc(self):
        return True

    @property
    def last_connection(self):
        last_message = self._text_buffer.last_message
        if last_message:
            return last_message.time
        return None

    @refresh_wrapper.always
    def go_to_next_hl(self):
        """
        Go to the next HL in the room, or the last
        """
        self.text_win.next_highlight()

    @refresh_wrapper.always
    def go_to_prev_hl(self):
        """
        Go to the previous HL in the room, or the first
        """
        self.text_win.previous_highlight()

    def completion_version(self, the_input):
        """Completion for /version"""
        compare_users = lambda x: x.last_talked
236
237
238
239
240
241
242
243
244
        userlist = []
        for user in sorted(self.users, key=compare_users, reverse=True):
            if user.nick != self.own_nick:
                userlist.append(user.nick)
        comp = []
        for jid in (jid for jid in roster.jids() if len(roster[jid])):
            for resource in roster[jid].resources:
                comp.append(resource.jid)
        comp.sort()
245
        userlist.extend(comp)
246

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

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

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

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

270
271
272
273
274
275
276
    def completion_color(self, the_input):
        """Completion for /color"""
        n = the_input.get_argument_position(quoted=True)
        if n == 1:
            userlist = [user.nick for user in self.users]
            if self.own_nick in userlist:
                userlist.remove(self.own_nick)
277
            return Completion(the_input.new_completion, userlist, 1, '', quotify=True)
278
279
280
        elif n == 2:
            colors = [i for i in xhtml.colors if i]
            colors.sort()
281
            colors.append('unset')
Eijebong's avatar
Eijebong committed
282
            colors.append('random')
283
            return Completion(the_input.new_completion, colors, 2, '', quotify=False)
284

285
286
287
288
289
290
    def completion_ignore(self, the_input):
        """Completion for /ignore"""
        userlist = [user.nick for user in self.users]
        if self.own_nick in userlist:
            userlist.remove(self.own_nick)
        userlist.sort()
291
        return Completion(the_input.auto_completion, userlist, quotify=False)
292
293
294
295
296
297
298
299

    def completion_role(self, the_input):
        """Completion for /role"""
        n = the_input.get_argument_position(quoted=True)
        if n == 1:
            userlist = [user.nick for user in self.users]
            if self.own_nick in userlist:
                userlist.remove(self.own_nick)
300
            return Completion(the_input.new_completion, userlist, 1, '', quotify=True)
301
302
        elif n == 2:
            possible_roles = ['none', 'visitor', 'participant', 'moderator']
303
            return Completion(the_input.new_completion, possible_roles, 2, '',
304
                                            quotify=True)
305
306
307
308
309
310
311
312
313
314
315
316

    def completion_affiliation(self, the_input):
        """Completion for /affiliation"""
        n = the_input.get_argument_position(quoted=True)
        if n == 1:
            userlist = [user.nick for user in self.users]
            if self.own_nick in userlist:
                userlist.remove(self.own_nick)
            jidlist = [user.jid.bare for user in self.users]
            if self.core.xmpp.boundjid.bare in jidlist:
                jidlist.remove(self.core.xmpp.boundjid.bare)
            userlist.extend(jidlist)
317
            return Completion(the_input.new_completion, userlist, 1, '', quotify=True)
318
        elif n == 2:
319
320
            possible_affiliations = ['none', 'member', 'admin',
                                     'owner', 'outcast']
321
            return Completion(the_input.new_completion, possible_affiliations, 2, '',
322
                                            quotify=True)
323

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

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

    def scroll_user_list_up(self):
        self.user_win.scroll_up()
        self.user_win.refresh(self.users)
        self.input.refresh()

    def scroll_user_list_down(self):
        self.user_win.scroll_down()
        self.user_win.refresh(self.users)
        self.input.refresh()

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

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

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

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

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

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

    def join(self):
        """
        Join the room
        """
        status = self.core.get_status()
        if self.last_connection:
            delta = datetime.now() - self.last_connection
            seconds = delta.seconds + delta.days * 24 * 3600
        else:
            seconds = 0
        muc.join_groupchat(self.core, self.name, self.own_nick,
                           self.password,
                           status=status.message,
                           show=status.show,
                           seconds=seconds)
440

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

487
488
489
490
491
    @command_args_parser.quoted(2, 2, [''])
    def command_color(self, args):
        """
        /color <nick> <color>
        Fix a color for a nick.
Eijebong's avatar
Eijebong committed
492
493
        Use "unset" instead of a color to remove the attribution.
        User "random" to attribute a random color.
494
495
        """
        if args is None:
496
            return self.core.command.help('color')
497
498
499
        nick = args[0]
        color = args[1].lower()
        user = self.get_user_by_name(nick)
Eijebong's avatar
Eijebong committed
500
        if not color in xhtml.colors and color not in ('unset', 'random'):
501
            return self.core.information("Unknown color: %s" % color, 'Error')
502
        if user and user.nick == self.own_nick:
503
504
            return self.core.information("You cannot change the color of your"
                                         " own nick.", 'Error')
505
506
        if color == 'unset':
            if config.remove_and_save(nick, 'muc_colors'):
507
                self.core.information('Color for nick %s unset' % (nick))
508
        else:
Eijebong's avatar
Eijebong committed
509
510
            if color == 'random':
                color = random.choice(list(xhtml.colors))
511
512
            if user:
                user.change_color(color)
513
            config.set_and_save(nick, color, 'muc_colors')
514
515
516
517
            nick_color_aliases = config.get_by_tabname('nick_color_aliases', self.name)
            if nick_color_aliases:
                # if any user in the room has a nick which is an alias of the
                # nick, update its color
518
519
520
521
522
523
                for tab in self.core.get_tabs(MucTab):
                    for u in tab.users:
                        nick_alias = re.sub('^_*', '', u.nick)
                        nick_alias = re.sub('_*$', '', nick_alias)
                        if nick_alias == nick:
                            u.change_color(color)
524
525
526
527
            self.text_win.rebuild_everything(self._text_buffer)
            self.user_win.refresh(self.users)
            self.text_win.refresh()
            self.input.refresh()
528

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

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

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

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

592
            if arg:
593
594
595
596
597
598
599
600
601
                msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
                       'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
                       ' left the chatroom'
                       ' (\x19o%(reason)s\x19%(info_col)s})') % {
                           'info_col': info_col, 'reason': arg,
                           'spec': char_quit, 'color': color,
                           'color_spec': spec_col,
                           'nick': self.own_nick,
                       }
602
            else:
603
604
605
606
607
608
609
610
                msg = ('\x19%(color_spec)s}%(spec)s\x19%(info_col)s} '
                       'You (\x19%(color)s}%(nick)s\x19%(info_col)s})'
                       ' left the chatroom') % {
                           'info_col': info_col,
                           'spec': char_quit, 'color': color,
                           'color_spec': spec_col,
                           'nick': self.own_nick,
                       }
611

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

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

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

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

653
654
    @command_args_parser.raw
    def command_topic(self, subject):
655
656
657
        """
        /topic [new topic]
        """
658
        if not subject:
mathieui's avatar
mathieui committed
659
660
661
662
663
664
665
666
667
668
669
670
671
            info_text = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
            norm_text = dump_tuple(get_theme().COLOR_NORMAL_TEXT)
            if self.topic_from:
                user = self.get_user_by_name(self.topic_from)
                if user:
                    user_text = dump_tuple(user.color)
                    user_string = '\x19%s}(set by \x19%s}%s\x19%s})' % (
                            info_text, user_text, user.nick, info_text)
                else:
                    user_string = self.topic_from
            else:
                user_string = ''

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

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

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

688
689
690
691
692
693
694
        aff = {
                'owner': get_theme().CHAR_AFFILIATION_OWNER,
                'admin': get_theme().CHAR_AFFILIATION_ADMIN,
                'member': get_theme().CHAR_AFFILIATION_MEMBER,
                'none': get_theme().CHAR_AFFILIATION_NONE,
                }

695
696
697
698
699
        colors = {}
        colors["visitor"] = dump_tuple(get_theme().COLOR_USER_VISITOR)
        colors["moderator"] = dump_tuple(get_theme().COLOR_USER_MODERATOR)
        colors["participant"] = dump_tuple(get_theme().COLOR_USER_PARTICIPANT)
        color_other = dump_tuple(get_theme().COLOR_USER_NONE)
700
701

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

709
710
711
712
713
714
715
716
717
        buff.append('\n')
        message = ' '.join(buff)

        self._text_buffer.add_message(message)
        self.text_win.refresh()
        self.input.refresh()

    def completion_topic(self, the_input):
        if the_input.get_argument_position() == 1:
718
            return Completion(the_input.auto_completion, [self.topic], '', quotify=False)
719
720
721
722
723

    def completion_quoted(self, the_input):
        """Nick completion, but with quotes"""
        if the_input.get_argument_position(quoted=True) == 1:
            compare_users = lambda x: x.last_talked
724
725
726
727
728
            word_list = []
            for user in sorted(self.users, key=compare_users, reverse=True):
                if user.nick != self.own_nick:
                    word_list.append(user.nick)

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

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

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

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

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

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

        nick, role, reason = args[0], args[1].lower(), args[2]

        valid_roles = ('none', 'visitor', 'participant', 'moderator')

        if not self.joined or role not in valid_roles:
790
791
            return self.core.information('The role must be one of ' + ', '.join(valid_roles),
                                         'Error')
792

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

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

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

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

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

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

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

833
    @command_args_parser.raw
834
835
836
837
838
839
    def command_say(self, line, correct=False):
        """
        /say <message>
        Or normal input + enter
        """
        needed = 'inactive' if self.inactive else 'active'
840
        msg = self.core.xmpp.make_message(self.name)
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
        msg['type'] = 'groupchat'
        msg['body'] = line
        # trigger the event BEFORE looking for colors.
        # This lets a plugin insert \x19xxx} colors, that will
        # be converted in xhtml.
        self.core.events.trigger('muc_say', msg, self)
        if not msg['body']:
            self.cancel_paused_delay()
            self.text_win.refresh()
            self.input.refresh()
            return
        if msg['body'].find('\x19') != -1:
            msg.enable('html')
            msg['html']['body'] = xhtml.poezio_colors_to_html(msg['body'])
            msg['body'] = xhtml.clean_text(msg['body'])
856
857
        if (config.get_by_tabname('send_chat_states', self.general_jid)
                and self.remote_wants_chatstates is not False):
858
859
860
861
862
863
864
865
866
867
868
869
870
871
            msg['chat_state'] = needed
        if correct:
            msg['replace']['id'] = self.last_sent_message['id']
        self.cancel_paused_delay()
        self.core.events.trigger('muc_say_after', msg, self)
        if not msg['body']:
            self.cancel_paused_delay()
            self.text_win.refresh()
            self.input.refresh()
            return
        self.last_sent_message = msg
        msg.send()
        self.chat_state = needed

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

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

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

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

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

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

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

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


942
943
944
945
946
947
948
        self.user_win.resize(self.height - 3 - info_win_height
                                - tab_win_height,
                             self.width - (self.width // 10) * 9 - 1,
                             1,
                             (self.width // 10) * 9 + 1)
        self.v_separator.resize(self.height - 3 - info_win_height - tab_win_height,
                                1, 1, 9 * (self.width // 10))
949

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

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

    def refresh(self):
        if self.need_resize:
            self.resize()
965
        log.debug('  TAB   Refresh: %s', self.__class__.__name__)
966
        if config.get('hide_user_list') or self.size.tab_degrade_x:
967
968
969
            display_user_list = False
        else:
            display_user_list = True
mathieui's avatar
mathieui committed
970
        display_info_win =