muctab.py 69.1 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
13
14
15
16
17
from gettext import gettext as _

import logging
log = logging.getLogger(__name__)

import curses
import os
import random
18
import re
mathieui's avatar
mathieui committed
19
from datetime import datetime
20
from functools import reduce
21
22
23
24
25
26
27
28
29
30
31

from . import ChatTab, Tab

import common
import fixes
import multiuserchat as muc
import timed_events
import windows
import xhtml
from common import safeJID
from config import config
32
from decorators import refresh_wrapper, command_args_parser
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
from logger import logger
from roster import roster
from theming import get_theme, dump_tuple
from user import User


SHOW_NAME = {
    'dnd': _('busy'),
    'away': _('away'),
    'xa': _('not available'),
    'chat': _('chatty'),
    '': _('available')
    }

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

        if self.core.xmpp.boundjid.server == "gmail.com": #gmail sucks
            del self.commands["nick"]

        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
280
281
    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()
            return the_input.new_completion(colors, 2, '', quotify=False)

282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
    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']
300
301
            return the_input.new_completion(possible_roles, 2, '',
                                            quotify=True)
302
303
304
305
306
307
308
309
310
311
312
313
314
315

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

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

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

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

378
379
    @command_args_parser.quoted(0)
    def command_configure(self, ignored):
380
381
382
        """
        /configure
        """
383
384
385
        def on_form_received(form):
            if not form:
                self.core.information(
386
387
                    _('Could not retrieve the configuration form'),
                    _('Error'))
388
389
390
                return
            self.core.open_new_form(form, self.cancel_config, self.send_config)

391
        fixes.get_room_form(self.core.xmpp, self.name, on_form_received)
392
393
394
395
396

    def cancel_config(self, form):
        """
        The user do not want to send his/her config, send an iq cancel
        """
397
        muc.cancel_config(self.core.xmpp, self.name)
398
399
400
401
402
403
        self.core.close_tab()

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

407
408
    @command_args_parser.raw
    def command_cycle(self, msg):
409
        """/cycle [reason]"""
louiz’'s avatar
louiz’ committed
410
        self.command_part(msg)
411
        self.disconnect()
412
        self.user_win.pos = 0
413
414
415
        self.core.disable_private_tabs(self.name)
        self.core.command_join('"/%s"' % self.own_nick)

416
417
    @command_args_parser.quoted(0, 1, [''])
    def command_recolor(self, args):
418
419
420
421
        """
        /recolor [random]
        Re-assign color to the participants of the room
        """
422
423
424
425
426
        deterministic = config.get_by_tabname('deterministic_nick_colors', self.name)
        if deterministic:
            for user in self.users:
                if user.nick == self.own_nick:
                    continue
427
428
429
                color = config.get_by_tabname(user.nick, 'muc_colors')
                if color != '':
                    continue
430
431
432
433
434
435
436
437
                user.set_deterministic_color()
            if args[0] == 'random':
                self.core.information(_('"random" was provided, but poezio is '
                                        'configured to use deterministic colors'),
                                        'Warning')
            self.user_win.refresh(self.users)
            self.input.refresh()
            return
438
439
440
        compare_users = lambda x: x.last_talked
        users = list(self.users)
        sorted_users = sorted(users, key=compare_users, reverse=True)
441
        full_sorted_users = sorted_users[:]
442
        # search our own user, to remove it from the list
443
444
445
        # Also remove users whose color is fixed
        for user in full_sorted_users:
            color = config.get_by_tabname(user.nick, 'muc_colors')
446
447
448
            if user.nick == self.own_nick:
                sorted_users.remove(user)
                user.color = get_theme().COLOR_OWN_NICK
449
450
451
            elif color != '':
                sorted_users.remove(user)
                user.change_color(color, deterministic)
452
        colors = list(get_theme().LIST_COLOR_NICKNAMES)
453
        if args[0] == 'random':
454
455
456
457
458
459
460
461
            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()

462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
    @command_args_parser.quoted(2, 2, [''])
    def command_color(self, args):
        """
        /color <nick> <color>
        Fix a color for a nick.
        """
        if args is None:
            return self.core.command_help('color')
        nick = args[0]
        color = args[1].lower()
        user = self.get_user_by_name(nick)
        if not user:
            return self.core.information(_("Unknown user: %s") % nick)
        if not color in xhtml.colors:
            return self.core.information(_("Unknown color: %s") % color, 'Error')
        if user.nick == self.own_nick:
            return self.core.information(_("You cannot change the color of your"
                                           " own nick.", 'Error'))
480
481
        user.change_color(color)
        config.write_in_file('muc_colors', nick, color)
482
483
484
485
486
        self.text_win.rebuild_everything(self._text_buffer)
        self.user_win.refresh(self.users)
        self.text_win.refresh()
        self.input.refresh()

487
488
    @command_args_parser.quoted(1)
    def command_version(self, args):
489
490
491
492
493
        """
        /version <jid or nick>
        """
        def callback(res):
            if not res:
494
495
496
497
498
                return self.core.information(_('Could not get the software '
                                               'version from %s') % (jid,),
                                             _('Warning'))
            version = _('%s is running %s version %s on %s') % (
                         jid,
499
500
501
502
                         res.get('name') or _('an unknown software'),
                         res.get('version') or _('unknown'),
                         res.get('os') or _('an unknown platform'))
            self.core.information(version, 'Info')
503
        if args is None:
504
            return self.core.command_help('version')
505
506
        nick = args[0]
        if nick in [user.nick for user in self.users]:
507
            jid = safeJID(self.name).bare
508
            jid = safeJID(jid + '/' + nick)
509
        else:
510
            jid = safeJID(nick)
511
        fixes.get_version(self.core.xmpp, jid,
512
                          callback=callback)
513

514
515
    @command_args_parser.quoted(1)
    def command_nick(self, args):
516
517
518
        """
        /nick <nickname>
        """
519
        if args is None:
520
            return self.core.command_help('nick')
521
        nick = args[0]
522
        if not self.joined:
523
524
            return self.core.information(_('/nick only works in joined rooms'),
                                         _('Info'))
525
        current_status = self.core.get_status()
526
        if not safeJID(self.name + '/' + nick):
527
            return self.core.information('Invalid nick', 'Info')
528
529
530
        muc.change_nick(self.core, self.name, nick,
                        current_status.message,
                        current_status.show)
531

532
533
    @command_args_parser.quoted(0, 1, [''])
    def command_part(self, args):
534
535
536
        """
        /part [msg]
        """
537
        arg = args[0]
538
539
        msg = None
        if self.joined:
540
541
542
543
            info_col = dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
            char_quit = get_theme().CHAR_QUIT
            spec_col = dump_tuple(get_theme().COLOR_QUIT_CHAR)

544
545
            if config.get_by_tabname('display_user_color_in_join_part',
                                     self.general_jid):
546
547
548
549
                color = dump_tuple(get_theme().COLOR_OWN_NICK)
            else:
                color = 3

550
            if arg:
551
552
553
554
555
556
557
558
559
                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,
                        }
560
            else:
561
562
563
564
565
566
567
568
569
                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,
                        }

570
            self.add_message(msg, typ=2)
571
572
            self.disconnect()
            muc.leave_groupchat(self.core.xmpp, self.name, self.own_nick, arg)
573
            self.core.disable_private_tabs(self.name, reason=msg)
574
575
576
577
            if self == self.core.current_tab():
                self.refresh()
            self.core.doupdate()

578
579
    @command_args_parser.raw
    def command_close(self, msg):
580
581
582
        """
        /close [msg]
        """
louiz’'s avatar
louiz’ committed
583
        self.command_part(msg)
584
585
        self.core.close_tab()

586
587
    @command_args_parser.quoted(1, 1)
    def command_query(self, args):
588
589
590
        """
        /query <nick> [message]
        """
591
592
        if args is None:
            return  self.core.command_help('query')
593
594
595
596
597
        nick = args[0]
        r = None
        for user in self.users:
            if user.nick == nick:
                r = self.core.open_private_window(self.name, user.nick)
598
        if r and len(args) == 2:
599
            msg = args[1]
600
601
            self.core.current_tab().command_say(
                    xhtml.convert_simple_to_full_colors(msg))
602
603
604
        if not r:
            self.core.information(_("Cannot find user: %s" % nick), 'Error')

605
606
    @command_args_parser.raw
    def command_topic(self, subject):
607
608
609
        """
        /topic [new topic]
        """
610
        if not subject:
611
            self._text_buffer.add_message(
612
                    _("\x19%s}The subject of the room is: %s %s") %
613
                        (dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
614
615
616
                         self.topic,
                         '(set by %s)' % self.topic_from if self.topic_from
                                                         else ''))
617
618
            self.refresh()
            return
619

620
621
        muc.change_subject(self.core.xmpp, self.name, subject)

622
623
    @command_args_parser.quoted(0)
    def command_names(self, args):
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
        """
        /names
        """
        if not self.joined:
            return
        color_visitor = dump_tuple(get_theme().COLOR_USER_VISITOR)
        color_other = dump_tuple(get_theme().COLOR_USER_NONE)
        color_moderator = dump_tuple(get_theme().COLOR_USER_MODERATOR)
        color_participant = dump_tuple(get_theme().COLOR_USER_PARTICIPANT)
        visitors, moderators, participants, others = [], [], [], []
        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,
                }

        users = self.users[:]
        users.sort(key=lambda x: x.nick.lower())
        for user in users:
644
645
            color = aff.get(user.affiliation,
                            get_theme().CHAR_AFFILIATION_NONE)
646
647
648
649
650
651
652
653
654
655
656
            if user.role == 'visitor':
                visitors.append((user, color))
            elif user.role == 'participant':
                participants.append((user, color))
            elif user.role == 'moderator':
                moderators.append((user, color))
            else:
                others.append((user, color))

        buff = ['Users: %s \n' % len(self.users)]
        for moderator in moderators:
657
658
659
            buff.append('\x19%s}%s\x19o\x19%s}%s\x19o' % (
                    color_moderator, moderator[1],
                    dump_tuple(moderator[0].color), moderator[0].nick))
660
        for participant in participants:
661
662
663
            buff.append('\x19%s}%s\x19o\x19%s}%s\x19o' % (
                    color_participant, participant[1],
                    dump_tuple(participant[0].color), participant[0].nick))
664
        for visitor in visitors:
665
666
667
            buff.append('\x19%s}%s\x19o\x19%s}%s\x19o' % (
                    color_visitor, visitor[1],
                    dump_tuple(visitor[0].color), visitor[0].nick))
668
        for other in others:
669
670
671
            buff.append('\x19%s}%s\x19o\x19%s}%s\x19o' % (
                    color_other, other[1],
                    dump_tuple(other[0].color), other[0].nick))
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
        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
687
688
689
690
691
            word_list = []
            for user in sorted(self.users, key=compare_users, reverse=True):
                if user.nick != self.own_nick:
                    word_list.append(user.nick)

692
693
            return the_input.new_completion(word_list, 1, quotify=True)

694
695
    @command_args_parser.quoted(1, 1)
    def command_kick(self, args):
696
697
698
        """
        /kick <nick> [reason]
        """
699
700
701
702
        if args is None:
            return self.core.command_help('kick')
        if len(args) == 2:
            msg = ' "%s"' % args[1]
703
        else:
704
705
            msg = ''
        self.command_role('"'+args[0]+ '" none'+msg)
706

707
708
    @command_args_parser.quoted(1, 1)
    def command_ban(self, args):
709
710
711
712
713
        """
        /ban <nick> [reason]
        """
        def callback(iq):
            if iq['type'] == 'error':
714
                self.core.room_error(iq, self.name)
715
        if args is None:
716
717
718
719
720
721
722
723
            return self.core.command_help('ban')
        if len(args) > 1:
            msg = args[1]
        else:
            msg = ''
        nick = args[0]

        if nick in [user.nick for user in self.users]:
724
            res = muc.set_user_affiliation(self.core.xmpp, self.name,
725
726
                                           'outcast', nick=nick,
                                           callback=callback, reason=msg)
727
        else:
728
            res = muc.set_user_affiliation(self.core.xmpp, self.name,
729
730
                                           'outcast', jid=safeJID(nick),
                                           callback=callback, reason=msg)
731
732
733
        if not res:
            self.core.information('Could not ban user', 'Error')

734
735
    @command_args_parser.quoted(2, 1, [''])
    def command_role(self, args):
736
737
738
739
740
741
742
        """
        /role <nick> <role> [reason]
        Changes the role of an user
        roles can be: none, visitor, participant, moderator
        """
        def callback(iq):
            if iq['type'] == 'error':
743
                self.core.room_error(iq, self.name)
744
745
746
747
748
749
750
751
752
753
754
755

        if args is None:
            return self.core.command_help('role')

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

756
        if not safeJID(self.name + '/' + nick):
mathieui's avatar
mathieui committed
757
            return self.core.information('Invalid nick', 'Info')
758
        muc.set_user_role(self.core.xmpp, self.name, nick, reason, role,
759
                          callback=callback)
760

761
762
    @command_args_parser.quoted(2)
    def command_affiliation(self, args):
763
764
765
766
767
768
769
        """
        /affiliation <nick> <role>
        Changes the affiliation of an user
        affiliations can be: outcast, none, member, admin, owner
        """
        def callback(iq):
            if iq['type'] == 'error':
770
                self.core.room_error(iq, self.name)
771
772
773
774

        if args is None:
            return self.core.command_help('affiliation')

775
        nick, affiliation = args[0], args[1].lower()
776

777
778
        if not self.joined:
            return
779
780
781
782
783
784

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

785
        if nick in [user.nick for user in self.users]:
786
            res = muc.set_user_affiliation(self.core.xmpp, self.name,
787
788
                                           affiliation, nick=nick,
                                           callback=callback)
789
        else:
790
            res = muc.set_user_affiliation(self.core.xmpp, self.name,
791
792
                                           affiliation, jid=safeJID(nick),
                                           callback=callback)
793
        if not res:
794
            self.core.information(_('Could not set affiliation'), _('Error'))
795

796
    @command_args_parser.raw
797
798
799
800
801
802
    def command_say(self, line, correct=False):
        """
        /say <message>
        Or normal input + enter
        """
        needed = 'inactive' if self.inactive else 'active'
803
        msg = self.core.xmpp.make_message(self.name)
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
        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'])
819
820
        if (config.get_by_tabname('send_chat_states', self.general_jid)
                and self.remote_wants_chatstates is not False):
821
822
823
824
825
826
827
828
829
830
831
832
833
834
            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

835
836
837
    @command_args_parser.raw
    def command_xhtml(self, msg):
        message = self.generate_xhtml_message(msg)
838
839
840
841
        if message:
            message['type'] = 'groupchat'
            message.send()

842
843
    @command_args_parser.quoted(1)
    def command_ignore(self, args):
844
845
846
        """
        /ignore <nick>
        """
847
        if args is None:
848
849
850
            return self.core.command_help('ignore')

        nick = args[0]
851
852
853
854
855
856
857
858
859
        user = self.get_user_by_name(nick)
        if not user:
            self.core.information(_('%s is not in the room') % nick)
        elif user in self.ignores:
            self.core.information(_('%s is already ignored') % nick)
        else:
            self.ignores.append(user)
            self.core.information(_("%s is now ignored") % nick, 'info')

860
861
    @command_args_parser.quoted(1)
    def command_unignore(self, args):
862
863
864
        """
        /unignore <nick>
        """
865
        if args is None:
866
867
868
            return self.core.command_help('unignore')

        nick = args[0]
869
870
871
872
873
874
875
876
877
878
879
        user = self.get_user_by_name(nick)
        if not user:
            self.core.information(_('%s is not in the room') % nick)
        elif user not in self.ignores:
            self.core.information(_('%s is not ignored') % nick)
        else:
            self.ignores.remove(user)
            self.core.information(_('%s is now unignored') % nick)

    def completion_unignore(self, the_input):
        if the_input.get_argument_position() == 1:
880
881
            users = [user.nick for user in self.ignores]
            return the_input.auto_completion(users, quotify=False)
882
883
884
885
886

    def resize(self):
        """
        Resize the whole window. i.e. all its sub-windows
        """
887
        self.need_resize = False
888
        if config.get('hide_user_list') or self.size.tab_degrade_x:
889
            display_user_list = False
890
891
            text_width = self.width
        else:
892
893
894
895
896
897
898
899
900
901
902
903
904
            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


905
906
907
908
909
910
911
        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))
912

913
        self.topic_win.resize(1, self.width, 0, 0)
914
915
916
917

        self.text_win.resize(self.height - 3 - info_win_height
                                - tab_win_height,
                             text_width, 1, 0)
918
        self.text_win.rebuild_everything(self._text_buffer)
919
        self.info_header.resize(1, self.width,
920
921
                                self.height - 2 - info_win_height
                                    - tab_win_height,
922
                                0)
923
924
925
926
927
        self.input.resize(1, self.width, self.height-1, 0)

    def refresh(self):
        if self.need_resize:
            self.resize()
928
        log.debug('  TAB   Refresh: %s', self.__class__.__name__)
929
        if config.get('hide_user_list') or self.size.tab_degrade_x:
930
931
932
            display_user_list = False
        else:
            display_user_list = True
mathieui's avatar
mathieui committed
933
        display_info_win = not self.size.tab_degrade_y
934

935
936
        self.topic_win.refresh(self.get_single_line_topic())
        self.text_win.refresh()
937
        if display_user_list:
938
939
940
941
            self.v_separator.refresh()
            self.user_win.refresh(self.users)
        self.info_header.refresh(self, self.text_win)
        self.refresh_tab_win()
942
943
        if display_info_win:
            self.info_win.refresh()
944
945
946
947
948
949
950
        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)
951
952
953
954
        empty_after = self.input.get_text() == ''
        empty_after = empty_after or (self.input.get_text().startswith('/')
                                      and not
                                      self.input.get_text().startswith('//'))
955
956
957
958
959
960
961
962
963
964
        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

965
966
        # If we are not completing a command or a command argument,
        # complete a nick
967
        compare_users = lambda x: x.last_talked
968
969
970
971
        word_list = []
        for user in sorted(self.users, key=compare_users, reverse=True):
            if user.nick != self.own_nick:
                word_list.append(user.nick)
972
        after = config.get('after_completion') + ' '
973
        input_pos = self.input.pos
974
975
976
977
        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):
978
979
            add_after = after
        else:
980
            if not config.get('add_space_after_completion'):
981
982
983
                add_after = ''
            else:
                add_after = ' '
984
        self.input.auto_completion(word_list, add_after, quotify=False)
985
986
987
988
        empty_after = self.input.get_text() == ''
        empty_after = empty_after or (self.input.get_text().startswith('/')
                                      and not
                                      self.input.get_text().startswith('//'))
989
990
991
        self.send_composing_chat_state(empty_after)

    def get_nick(self):
992
        if not config.get('show_muc_jid'):
993
994
995
996
997
998
999
1000
            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:
For faster browsing, not all history is shown. View entire blame