muctab.py 71.4 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
36
37


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

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

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

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

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

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

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

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

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

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

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

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

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

323
    @command_args_parser.quoted(1, 1, [''])
324
325
    def command_invite(self, args):
        """/invite <jid> [reason]"""
326
        if args is None:
327
            return self.core.command.help('invite')
328
        jid, reason = args
329
        self.core.command.invite('%s %s "%s"' % (jid, self.name, reason))
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346

    def completion_invite(self, the_input):
        """Completion for /invite"""
        n = the_input.get_argument_position(quoted=True)
        if n == 1:
            return the_input.new_completion(roster.jids(), 1, quotify=True)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

971
972
        self.topic_win.refresh(self.get_single_line_topic())
        self.text_win.refresh()
973
        if display_user_list:
974
975
            self.v_separator.refresh()
            self.user_win.refresh(self.users)
976
        self.info_header.refresh(self, self.text_win, user=self.own_user)
977
        self.refresh_tab_win()
978
979
        if display_info_win:
            self.info_win.refresh()
980
981
982
983
984
985
986
        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)
987
988
989
990
        empty_after = self.input.get_text() == ''
        empty_after = empty_after or (self.input.get_text().startswith('/')
                                      and not
                                      self.input.get_text().startswith('//'))
991
992
993
994
995
996
997
998
999
1000
        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

For faster browsing, not all history is shown. View entire blame