tabs.py 157 KB
Newer Older
louiz’'s avatar
louiz’ committed
1
# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org>
2
3
4
5
#
# This file is part of Poezio.
#
# Poezio is free software: you can redistribute it and/or modify
6
# it under the terms of the zlib license. See the COPYING file.
7
8

"""
9
a Tab object is a way to organize various Windows (see windows.py)
10
around the screen at once.
11
A tab is then composed of multiple Buffers.
12
Each Tab object has different refresh() and resize() methods, defining how its
13
Windows are displayed, resized, etc.
14
15
"""

16
17
MIN_WIDTH = 42
MIN_HEIGHT = 6
18

19
20
21
import logging
log = logging.getLogger(__name__)

louiz’'s avatar
Typo    
louiz’ committed
22
from gettext import gettext as _
louiz’'s avatar
louiz’ committed
23

24
import windows
25
import curses
mathieui's avatar
mathieui committed
26
import fixes
27
import difflib
louiz’'s avatar
louiz’ committed
28
import string
29
import common
louiz’'s avatar
louiz’ committed
30
31
import core
import singleton
32
import random
33
import xhtml
34
35
import weakref
import timed_events
mathieui's avatar
mathieui committed
36
import os
37
import time
38

louiz’'s avatar
louiz’ committed
39
40
import multiuserchat as muc

41
from theming import get_theme, dump_tuple
42

43
from common import safeJID
44
from decorators import refresh_wrapper
45
from sleekxmpp import JID, InvalidJID
mathieui's avatar
mathieui committed
46
47
from sleekxmpp.xmlstream import matcher
from sleekxmpp.xmlstream.handler import Callback
48
from config import config
49
from roster import RosterGroup, roster
50
from contact import Contact, Resource
mathieui's avatar
mathieui committed
51
from text_buffer import TextBuffer, CorrectionError
52
from user import User
mathieui's avatar
mathieui committed
53
from os import getenv, path
54
from logger import logger
55

56
from datetime import datetime, timedelta
mathieui's avatar
mathieui committed
57
from xml.etree import cElementTree as ET
58

59
60
61
62
63
64
65
66
SHOW_NAME = {
    'dnd': _('busy'),
    'away': _('away'),
    'xa': _('not available'),
    'chat': _('chatty'),
    '': _('available')
    }

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

69
STATE_COLORS = {
mathieui's avatar
mathieui committed
70
        'disconnected': lambda: get_theme().COLOR_TAB_DISCONNECTED,
mathieui's avatar
mathieui committed
71
        'scrolled': lambda: get_theme().COLOR_TAB_SCROLLED,
72
        'joined': lambda: get_theme().COLOR_TAB_JOINED,
mathieui's avatar
mathieui committed
73
74
75
76
77
        'message': lambda: get_theme().COLOR_TAB_NEW_MESSAGE,
        'highlight': lambda: get_theme().COLOR_TAB_HIGHLIGHT,
        'private': lambda: get_theme().COLOR_TAB_PRIVATE,
        'normal': lambda: get_theme().COLOR_TAB_NORMAL,
        'current': lambda: get_theme().COLOR_TAB_CURRENT,
78
        'attention': lambda: get_theme().COLOR_TAB_ATTENTION,
79
80
    }

louiz’'s avatar
louiz’ committed
81
82
VERTICAL_STATE_COLORS = {
        'disconnected': lambda: get_theme().COLOR_VERTICAL_TAB_DISCONNECTED,
mathieui's avatar
mathieui committed
83
        'scrolled': lambda: get_theme().COLOR_VERTICAL_TAB_SCROLLED,
84
        'joined': lambda: get_theme().COLOR_VERTICAL_TAB_JOINED,
louiz’'s avatar
louiz’ committed
85
86
87
88
89
        'message': lambda: get_theme().COLOR_VERTICAL_TAB_NEW_MESSAGE,
        'highlight': lambda: get_theme().COLOR_VERTICAL_TAB_HIGHLIGHT,
        'private': lambda: get_theme().COLOR_VERTICAL_TAB_PRIVATE,
        'normal': lambda: get_theme().COLOR_VERTICAL_TAB_NORMAL,
        'current': lambda: get_theme().COLOR_VERTICAL_TAB_CURRENT,
90
        'attention': lambda: get_theme().COLOR_VERTICAL_TAB_ATTENTION,
louiz’'s avatar
louiz’ committed
91
92
93
    }


94
95
96
STATE_PRIORITY = {
        'normal': -1,
        'current': -1,
mathieui's avatar
mathieui committed
97
        'disconnected': 0,
mathieui's avatar
mathieui committed
98
        'scrolled': 0.5,
99
        'message': 1,
100
        'joined': 1,
101
102
        'highlight': 2,
        'private': 2,
103
        'attention': 3
104
105
    }

106
class Tab(object):
louiz’'s avatar
louiz’ committed
107
    tab_core = None
mathieui's avatar
mathieui committed
108

louiz’'s avatar
louiz’ committed
109
    def __init__(self):
110
        self.input = None
mathieui's avatar
mathieui committed
111
112
113
114
115
        if isinstance(self, MucTab) and not self.joined:
            self._state = 'disconnected'
        else:
            self._state = 'normal'

116
        self.need_resize = False
117
        self.need_resize = False
118
119
        self.key_func = {}      # each tab should add their keys in there
                                # and use them in on_input
120
        self.commands = {}      # and their own commands
121

122

louiz’'s avatar
louiz’ committed
123
124
125
126
127
128
    @property
    def core(self):
        if not Tab.tab_core:
            Tab.tab_core = singleton.Singleton(core.Core)
        return Tab.tab_core

mathieui's avatar
mathieui committed
129
130
131
132
133
134
135
    @property
    def nb(self):
        for index, tab in enumerate(self.core.tabs):
            if tab == self:
                return index
        return len(self.core.tabs)

mathieui's avatar
mathieui committed
136
137
138
139
140
141
    @property
    def tab_win(self):
        if not Tab.tab_core:
            Tab.tab_core = singleton.Singleton(core.Core)
        return Tab.tab_core.tab_win

louiz’'s avatar
louiz’ committed
142
143
144
145
146
147
    @property
    def left_tab_win(self):
        if not Tab.tab_core:
            Tab.tab_core = singleton.Singleton(core.Core)
        return Tab.tab_core.left_tab_win

148
149
150
151
152
153
154
155
156
    @staticmethod
    def tab_win_height():
        """
        Returns 1 or 0, depending on if we are using the vertical tab list
        or not.
        """
        if config.get('enable_vertical_tab_list', 'false') == 'true':
            return 0
        return 1
louiz’'s avatar
louiz’ committed
157

158
159
160
161
    @property
    def info_win(self):
        return self.core.information_win

162
163
    @property
    def color(self):
164
        return STATE_COLORS[self._state]()
165

louiz’'s avatar
louiz’ committed
166
167
168
169
    @property
    def vertical_color(self):
        return VERTICAL_STATE_COLORS[self._state]()

170
171
172
173
174
175
176
    @property
    def state(self):
        return self._state

    @state.setter
    def state(self, value):
        if not value in STATE_COLORS:
177
            log.debug("Invalid value for tab state: %s", value)
178
        elif STATE_PRIORITY[value] < STATE_PRIORITY[self._state] and \
mathieui's avatar
mathieui committed
179
180
                value not in ('current', 'disconnected') and \
                not (self._state == 'scrolled' and value == 'disconnected'):
181
182
183
            log.debug("Did not set state because of lower priority, asked: %s, kept: %s", value, self._state)
        elif self._state == 'disconnected' and value not in ('joined', 'current'):
            log.debug('Did not set state because disconnected tabs remain visible')
184
185
        else:
            self._state = value
186

187
188
189
    @staticmethod
    def resize(scr):
        Tab.size = (Tab.height, Tab.width) = scr.getmaxyx()
190
191
192
193
        if Tab.height < MIN_HEIGHT or Tab.width < MIN_WIDTH:
            Tab.visible = False
        else:
            Tab.visible = True
louiz’'s avatar
louiz’ committed
194
        windows.Win._tab_win = scr
195

mathieui's avatar
mathieui committed
196
197
198
199
200
201
202
203
204
205
    def register_command(self, name, func, *, desc='', shortdesc='', completion=None, usage=''):
        """
        Add a command
        """
        if name in self.commands:
            return
        if not desc and shortdesc:
            desc = shortdesc
        self.commands[name] = core.Command(func, desc, completion, shortdesc, usage)

206
207
208
209
210
211
212
213
214
215
216
217
    def complete_commands(self, the_input):
        """
        Does command completion on the specified input for both global and tab-specific
        commands.
        This should be called from the completion method (on tab, for example), passing
        the input where completion is to be made.
        It can completion the command name itself or an argument of the command.
        Returns True if a completion was made, False else.
        """
        txt = the_input.get_text()
        # check if this is a command
        if txt.startswith('/') and not txt.startswith('//'):
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
            position = the_input.get_argument_position(quoted=False)
            if position == 0:
                words = ['/%s'% (name) for name in sorted(self.core.commands)] +\
                    ['/%s' % (name) for name in sorted(self.commands)]
                the_input.new_completion(words, 0)
                # Do not try to cycle command completion if there was only
                # one possibily. The next tab will complete the argument.
                # Otherwise we would need to add a useless space before being
                # able to complete the arguments.
                hit_copy = set(the_input.hit_list)
                while not hit_copy:
                    whitespace = the_input.text.find(' ')
                    if whitespace == -1:
                        whitespace = len(the_input.text)
                    the_input.text = the_input.text[:whitespace-1] + the_input.text[whitespace:]
                    the_input.new_completion(words, 0)
                    hit_copy = set(the_input.hit_list)
                if len(hit_copy) == 1:
                    the_input.do_command(' ')
                    the_input.reset_completion()
                return True
239
            # check if we are in the middle of the command name
240
            elif len(txt.split()) > 1 or\
241
242
                    (txt.endswith(' ') and not the_input.last_completion):
                command_name = txt.split()[0][1:]
mathieui's avatar
mathieui committed
243
                if command_name in self.commands:
244
                    command = self.commands[command_name]
mathieui's avatar
mathieui committed
245
246
                elif command_name in self.core.commands:
                    command = self.core.commands[command_name]
247
248
249
                else:           # Unknown command, cannot complete
                    return False
                if command[2] is None:
250
                    return False # There's no completion function
251
252
253
254
                else:
                    return command[2](the_input)
                return True
        return False
255

256
    def execute_command(self, provided_text):
louiz’'s avatar
louiz’ committed
257
258
259
260
261
262
263
264
265
        """
        Execute the command in the input and return False if
        the input didn't contain a command
        """
        txt = provided_text or self.input.key_enter()
        if txt.startswith('/') and not txt.startswith('//') and\
                not txt.startswith('/me '):
            command = txt.strip().split()[0][1:]
            arg = txt[2+len(command):] # jump the '/' and the ' '
266
            func = None
267
            if command in self.commands: # check tab-specific commands
268
                func = self.commands[command][0]
269
            elif command in self.core.commands: # check global commands
270
                func = self.core.commands[command][0]
louiz’'s avatar
louiz’ committed
271
            else:
272
273
                low = command.lower()
                if low in self.commands:
274
                    func = self.commands[low][0]
275
                elif low in self.core.commands:
276
                    func = self.core.commands[low][0]
277
278
                else:
                    self.core.information(_("Unknown command (%s)") % (command), _('Error'))
279
280
281
282
283
284
            if command in ('correct', 'say'): # hack
                arg = xhtml.convert_simple_to_full_colors(arg)
            else:
                arg = xhtml.clean_text_simple(arg)
            if func:
                func(arg)
louiz’'s avatar
louiz’ committed
285
286
287
288
            return True
        else:
            return False

louiz’'s avatar
louiz’ committed
289
290
291
    def refresh_tab_win(self):
        if self.left_tab_win:
            self.left_tab_win.refresh()
292
293
        else:
            self.tab_win.refresh()
louiz’'s avatar
louiz’ committed
294

louiz’'s avatar
louiz’ committed
295
    def refresh(self):
296
297
298
        """
        Called on each screen refresh (when something has changed)
        """
299
        pass
300

301
302
303
304
    def get_name(self):
        """
        get the name of the tab
        """
305
        return self.__class__.__name__
306

mathieui's avatar
mathieui committed
307
308
309
310
311
312
    def get_nick(self):
        """
        Get the nick of the tab (defaults to its name)
        """
        return self.get_name()

313
314
315
316
317
318
    def get_text_window(self):
        """
        Returns the principal TextWin window, if there's one
        """
        return None

319
320
321
322
    def on_input(self, key, raw):
        """
        raw indicates if the key should activate the associated command or not.
        """
323
        pass
324

325
326
327
    def update_commands(self):
        for c in self.plugin_commands:
            if not c in self.commands:
mathieui's avatar
mathieui committed
328
                self.commands[c] = self.plugin_commands[c]
329

330
331
332
333
334
    def update_keys(self):
        for k in self.plugin_keys:
            if not k in self.key_func:
                self.key_func[k] = self.plugin_keys[k]

335
336
337
338
    def on_lose_focus(self):
        """
        called when this tab loses the focus.
        """
339
        self.state = 'normal'
340
341
342
343
344

    def on_gain_focus(self):
        """
        called when this tab gains the focus.
        """
345
        self.state = 'current'
346
347
348

    def on_scroll_down(self):
        """
349
        Defines what happens when we scroll down
350
        """
351
        pass
352
353
354

    def on_scroll_up(self):
        """
355
356
357
358
359
360
361
362
363
364
365
366
367
        Defines what happens when we scroll up
        """
        pass

    def on_line_up(self):
        """
        Defines what happens when we scroll one line up
        """
        pass

    def on_line_down(self):
        """
        Defines what happens when we scroll one line up
368
        """
369
        pass
370

mathieui's avatar
mathieui committed
371
372
373
374
375
376
377
378
379
380
381
382
    def on_half_scroll_down(self):
        """
        Defines what happens when we scroll half a screen down
        """
        pass

    def on_half_scroll_up(self):
        """
        Defines what happens when we scroll half a screen up
        """
        pass

383
    def on_info_win_size_changed(self):
384
385
386
        """
        Called when the window with the informations is resized
        """
387
        pass
388

389
390
391
392
    def on_close(self):
        """
        Called when the tab is to be closed
        """
393
394
        if self.input:
            self.input.on_delete()
395

louiz’'s avatar
louiz’ committed
396
397
398
399
400
401
402
403
404
    def matching_names(self):
        """
        Returns a list of strings that are used to name a tab with the /win
        command.  For example you could switch to a tab that returns
        ['hello', 'coucou'] using /win hel, or /win coucou
        If not implemented in the tab, it just doesn’t match with anything.
        """
        return []

405
    def __del__(self):
406
        log.debug('------ Closing tab %s', self.__class__.__name__)
407

mathieui's avatar
mathieui committed
408
409
410
411
412
413
414
415
416
417
418
class GapTab(Tab):

    def __bool__(self):
        return False

    def __len__(self):
        return 0

    def get_name(self):
        return ''

419
420
421
    def refresh(self):
        log.debug('WARNING: refresh() called on a gap tab, this should not happen')

422
423
class ChatTab(Tab):
    """
424
425
    A tab containing a chat of any type.
    Just use this class instead of Tab if the tab needs a recent-words completion
426
    Also, ^M is already bound to on_enter
427
    And also, add the /say command
428
    """
429
    plugin_commands = {}
430
    plugin_keys = {}
431
    def __init__(self, jid=''):
louiz’'s avatar
louiz’ committed
432
        Tab.__init__(self)
433
        self.name = jid
434
        self._text_buffer = TextBuffer()
435
436
437
438
        self.remote_wants_chatstates = None # change this to True or False when
        # we know that the remote user wants chatstates, or not.
        # None means we don’t know yet, and we send only "active" chatstates
        self.chatstate = None   # can be "active", "composing", "paused", "gone", "inactive"
439
440
441
442
443
444
        # We keep a weakref of the event that will set our chatstate to "paused", so that
        # we can delete it or change it if we need to
        self.timed_event_paused = None
        # if that’s None, then no paused chatstate was sent recently
        # if that’s a weakref returning None, then a paused chatstate was sent
        # since the last input
445
        self.remote_supports_attention = False
446
447
        # Keeps the last sent message to complete it easily in completion_correct, and to replace it.
        self.last_sent_message = None
448
        self.key_func['M-v'] = self.move_separator
449
        self.key_func['M-h'] = self.scroll_separator
450
451
        self.key_func['M-/'] = self.last_words_completion
        self.key_func['^M'] = self.on_enter
mathieui's avatar
mathieui committed
452
453
454
455
456
457
458
459
460
461
462
463
        self.register_command('say', self.command_say,
                usage=_('<message>'),
                shortdesc=_('Send the message.'))
        self.register_command('xhtml', self.command_xhtml,
                usage=_('<custom xhtml>'),
                shortdesc=_('Send custom XHTML.'))
        self.register_command('clear', self.command_clear,
                shortdesc=_('Clear the current buffer.'))
        self.register_command('correct', self.command_correct,
                desc=_('Fix the last message with whatever you want.'),
                shortdesc=_('Correct the last message.'),
                completion=self.completion_correct)
mathieui's avatar
mathieui committed
464
        self.chat_state = None
465
        self.update_commands()
466
        self.update_keys()
467

468
        # Get the logs
469
        log_nb = config.get('load_log', 10)
470
471
472
473
474
475

        if isinstance(self, PrivateTab):
            logs = logger.get_logs(safeJID(self.get_name()).full.replace('/', '\\'), log_nb)
        else:
            logs = logger.get_logs(safeJID(self.get_name()).bare, log_nb)
        if logs:
476
477
            for message in logs:
                self._text_buffer.add_message(**message)
478
479

    def log_message(self, txt, nickname, time=None, typ=1):
480
        """
mathieui's avatar
mathieui committed
481
        Log the messages in the archives.
482
        """
483
484
        name = safeJID(self.name).bare
        if not logger.log_message(name, nickname, txt, date=time, typ=typ):
mathieui's avatar
mathieui committed
485
            self.core.information(_('Unable to write in the log file'), 'Error')
486

487
488
    def add_message(self, txt, time=None, nickname=None, forced_user=None, nick_color=None, identifier=None, jid=None, history=None, typ=1):
        self.log_message(txt, nickname, time=time, typ=typ)
mathieui's avatar
mathieui committed
489
490
491
        self._text_buffer.add_message(txt, time=time,
                nickname=nickname,
                nick_color=nick_color,
492
                history=history,
mathieui's avatar
mathieui committed
493
                user=forced_user,
494
495
                identifier=identifier,
                jid=jid)
mathieui's avatar
mathieui committed
496

497
498
    def modify_message(self, txt, old_id, new_id, user=None,jid=None, nickname=None):
        self.log_message(txt, nickname, typ=1)
499
        message = self._text_buffer.modify_message(txt, old_id, new_id, time=time, user=user, jid=jid)
mathieui's avatar
mathieui committed
500
501
502
503
        if message:
            self.text_win.modify_message(old_id, message)
            self.core.refresh_window()
            return True
mathieui's avatar
mathieui committed
504
505
        return False

506
507
508
509
510
    def last_words_completion(self):
        """
        Complete the input with words recently said
        """
        # build the list of the recent words
511
        char_we_dont_want = string.punctuation+' ’„“”…«»'
512
        words = list()
513
        for msg in self._text_buffer.messages[:-40:-1]:
514
515
            if not msg:
                continue
516
            txt = xhtml.clean_text(msg.txt)
517
            for char in char_we_dont_want:
518
519
                txt = txt.replace(char, ' ')
            for word in txt.split():
520
521
                if len(word) >= 4 and word not in words:
                    words.append(word)
522
        words.extend([word for word in config.get('words', '').split(':') if word])
523
        self.input.auto_completion(words, ' ', quotify=False)
524
525

    def on_enter(self):
526
        txt = self.input.key_enter()
mathieui's avatar
mathieui committed
527
        if txt:
528
            if not self.execute_command(txt):
mathieui's avatar
mathieui committed
529
530
                if txt.startswith('//'):
                    txt = txt[1:]
531
                self.command_say(xhtml.convert_simple_to_full_colors(txt))
532
        self.cancel_paused_delay()
533

mathieui's avatar
mathieui committed
534
535
536
537
538
539
540
541
    def command_xhtml(self, arg):
        """"
        /xhtml <custom xhtml>
        """
        if not arg:
            return
        try:
            body = xhtml.clean_text(xhtml.xhtml_to_poezio_colors(arg))
542
543
            # The <body /> element is the only allowable child of the <xhtm-im>
            arg = "<body xmlns='http://www.w3.org/1999/xhtml'>%s</body>" % (arg,)
mathieui's avatar
mathieui committed
544
545
546
            ET.fromstring(arg)
        except:
            self.core.information('Could not send custom xhtml', 'Error')
547
            log.error('/xhtml: Unable to send custom xhtml', exc_info=True)
mathieui's avatar
mathieui committed
548
549
            return

550
        msg = self.core.xmpp.make_message(self.get_dest_jid())
mathieui's avatar
mathieui committed
551
        msg['body'] = body
mathieui's avatar
mathieui committed
552
553
        msg.enable('html')
        msg['html']['body'] = arg
mathieui's avatar
mathieui committed
554
555
556
557
558
559
560
        if isinstance(self, MucTab):
            msg['type'] = 'groupchat'
        if isinstance(self, ConversationTab):
            self.core.add_message_to_text_buffer(self._text_buffer, body, None, self.core.own_nick)
            self.refresh()
        msg.send()

561
562
563
    def get_dest_jid(self):
        return self.get_name()

564
    @refresh_wrapper.always
565
566
567
568
569
570
571
    def command_clear(self, args):
        """
        /clear
        """
        self._text_buffer.messages = []
        self.text_win.rebuild_everything(self._text_buffer)

572
    def send_chat_state(self, state, always_send=False):
573
574
575
        """
        Send an empty chatstate message
        """
576
        if not isinstance(self, MucTab) or self.joined:
mathieui's avatar
mathieui committed
577
            if state in ('active', 'inactive', 'gone') and self.inactive and not always_send:
578
                return
579
580
581
582
583
584
585
            if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) and \
                    self.remote_wants_chatstates is not False:
                msg = self.core.xmpp.make_message(self.get_dest_jid())
                msg['type'] = self.message_type
                msg['chat_state'] = state
                self.chat_state = state
                msg.send()
586

587
    def send_composing_chat_state(self, empty_after):
588
589
590
591
        """
        Send the "active" or "composing" chatstate, depending
        on the the current status of the input
        """
592
593
        name = self.general_jid
        if config.get_by_tabname('send_chat_states', 'true', name, True) == 'true' and self.remote_wants_chatstates:
mathieui's avatar
mathieui committed
594
            needed = 'inactive' if self.inactive else 'active'
595
596
597
598
            self.cancel_paused_delay()
            if not empty_after:
                if self.chat_state != "composing":
                    self.send_chat_state("composing")
599
                self.set_paused_delay(True)
600
601
            elif empty_after and self.chat_state != needed:
                self.send_chat_state(needed, True)
602

603
604
605
606
607
    def set_paused_delay(self, composing):
        """
        we create a timed event that will put us to paused
        in a few seconds
        """
608
        if config.get_by_tabname('send_chat_states', 'true', self.general_jid, True) != 'true':
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
            return
        if self.timed_event_paused:
            # check the weakref
            event = self.timed_event_paused()
            if event:
                # the event already exists: we just update
                # its date
                event.change_date(datetime.now() + timedelta(seconds=4))
                return
        new_event = timed_events.DelayedEvent(4, self.send_chat_state, 'paused')
        self.core.add_timed_event(new_event)
        self.timed_event_paused = weakref.ref(new_event)

    def cancel_paused_delay(self):
        """
        Remove that event from the list and set it to None.
        Called for example when the input is emptied, or when the message
        is sent
        """
        if self.timed_event_paused:
            event = self.timed_event_paused()
            if event:
631
                self.core.remove_timed_event(event)
632
633
634
                del event
        self.timed_event_paused = None

635
636
637
638
639
640
641
642
643
644
645
646
647
    def command_correct(self, line):
        """
        /correct <fixed message>
        """
        if not line:
            self.core.command_help('correct')
            return
        if not self.last_sent_message:
            self.core.information(_('There is no message to correct.'))
            return
        self.command_say(line, correct=True)

    def completion_correct(self, the_input):
648
        if self.last_sent_message and the_input.get_argument_position() == 1:
649
            return the_input.auto_completion([self.last_sent_message['body']], '', quotify=False)
650

mathieui's avatar
mathieui committed
651
652
653
654
655
656
    @property
    def inactive(self):
        """Whether we should send inactive or active as a chatstate"""
        return self.core.status.show in ('xa', 'away') or\
                (hasattr(self, 'directed_presence') and not self.directed_presence)

657
658
    def move_separator(self):
        self.text_win.remove_line_separator()
659
        self.text_win.add_line_separator(self._text_buffer)
660
        self.text_win.refresh()
661
662
        self.input.refresh()

663
    def get_conversation_messages(self):
664
        return self._text_buffer.messages
665

mathieui's avatar
mathieui committed
666
667
668
669
    def check_scrolled(self):
        if self.text_win.pos != 0:
            self.state = 'scrolled'

670
    def command_say(self, line, correct=False):
671
        pass
672

673
    def on_line_up(self):
mathieui's avatar
mathieui committed
674
        return self.text_win.scroll_up(1)
675
676

    def on_line_down(self):
mathieui's avatar
mathieui committed
677
        return self.text_win.scroll_down(1)
678
679

    def on_scroll_up(self):
mathieui's avatar
mathieui committed
680
        return self.text_win.scroll_up(self.text_win.height-1)
681
682

    def on_scroll_down(self):
mathieui's avatar
mathieui committed
683
        return self.text_win.scroll_down(self.text_win.height-1)
684

mathieui's avatar
mathieui committed
685
    def on_half_scroll_up(self):
mathieui's avatar
mathieui committed
686
        return self.text_win.scroll_up((self.text_win.height-1) // 2)
mathieui's avatar
mathieui committed
687
688

    def on_half_scroll_down(self):
mathieui's avatar
mathieui committed
689
        return self.text_win.scroll_down((self.text_win.height-1) // 2)
mathieui's avatar
mathieui committed
690

691
    @refresh_wrapper.always
692
693
694
    def scroll_separator(self):
        self.text_win.scroll_to_separator()

695
class MucTab(ChatTab):
696
697
698
699
    """
    The tab containing a multi-user-chat room.
    It contains an userlist, an input, a topic, an information and a chat zone
    """
700
    message_type = 'groupchat'
701
    plugin_commands = {}
702
    plugin_keys = {}
louiz’'s avatar
louiz’ committed
703
    def __init__(self, jid, nick):
mathieui's avatar
mathieui committed
704
        self.joined = False
705
        ChatTab.__init__(self, jid)
louiz’'s avatar
louiz’ committed
706
707
708
        self.own_nick = nick
        self.name = jid
        self.users = []
mathieui's avatar
mathieui committed
709
        self.privates = [] # private conversations
louiz’'s avatar
louiz’ committed
710
        self.topic = ''
711
712
713
714
        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.
715
716
        self.topic_win = windows.Topic()
        self.text_win = windows.TextWin()
louiz’'s avatar
louiz’ committed
717
        self._text_buffer.add_window(self.text_win)
718
719
720
721
        self.v_separator = windows.VerticalSeparator()
        self.user_win = windows.UserList()
        self.info_header = windows.MucInfoWin()
        self.input = windows.MessageInput()
722
723
        self.ignores = []       # set of Users
        # keys
724
        self.key_func['^I'] = self.completion
725
726
        self.key_func['M-u'] = self.scroll_user_list_down
        self.key_func['M-y'] = self.scroll_user_list_up
727
728
        self.key_func['M-n'] = self.go_to_next_hl
        self.key_func['M-p'] = self.go_to_prev_hl
729
        # commands
mathieui's avatar
mathieui committed
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
        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]'),
                desc=_('Kick the user with the specified nickname. You also can give an optional reason.'),
                shortdesc=_('Kick someone.'),
                completion=self.completion_quoted)
        self.register_command('ban', self.command_ban,
                usage=_('<nick> [reason]'),
                desc=_('Ban the user with the specified nickname. You also can give an optional reason.'),
                shortdesc='Ban someone',
                completion=self.completion_quoted)
        self.register_command('role', self.command_role,
                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.'),
                completion=self.completion_role)
        self.register_command('affiliation', self.command_affiliation,
                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.'),
                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]'),
                desc=_('Query: 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.'),
                completion=self.completion_quoted)
        self.register_command('part', self.command_part,
                usage=_('[message]'),
                desc=_('Disconnect from a room. You can specify an optional message.'),
                shortdesc=_('Leave the room.'))
        self.register_command('close', self.command_close,
                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.'))
        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,
                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.'),
                shortdesc=_('Change the nicks colors.'),
                completion=self.completion_recolor)
        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>'),
                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.'),
                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>'),
                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.'),
                completion=self.completion_version)
        self.register_command('names', self.command_names,
                desc=_('Get the list of the users in the room, and the list of the people assuming the different roles.'),
                shortdesc=_('List the users.'))
807
808
809
810
811
        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)
812
813
814
815

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

816
        self.resize()
817
        self.update_commands()
818
        self.update_keys()
819

820
821
822
823
    @property
    def general_jid(self):
        return self.get_name()

824
825
826
827
828
    @property
    def last_connection(self):
        last_message = self._text_buffer.last_message
        if last_message:
            return last_message.time
mathieui's avatar
mathieui committed
829
        return None
830

831
    @refresh_wrapper.always
832
833
834
835
836
837
    def go_to_next_hl(self):
        """
        Go to the next HL in the room, or the last
        """
        self.text_win.next_highlight()

838
    @refresh_wrapper.always
839
840
841
842
843
844
    def go_to_prev_hl(self):
        """
        Go to the previous HL in the room, or the first
        """
        self.text_win.previous_highlight()

mathieui's avatar
mathieui committed
845
846
    def completion_version(self, the_input):
        """Completion for /version"""
mathieui's avatar
mathieui committed
847
848
849
        compare_users = lambda x: x.last_talked
        userlist = [user.nick for user in sorted(self.users, key=compare_users, reverse=True)\
                         if user.nick != self.own_nick]
850
        return the_input.auto_completion(userlist, quotify=False)
mathieui's avatar
mathieui committed
851

mathieui's avatar
mathieui committed
852
853
854
855
    def completion_info(self, the_input):
        """Completion for /info"""
        compare_users = lambda x: x.last_talked
        userlist = [user.nick for user in sorted(self.users, key=compare_users, reverse=True)]
856
        return the_input.auto_completion(userlist, quotify=False)
mathieui's avatar
mathieui committed
857

mathieui's avatar
mathieui committed
858
859
860
    def completion_nick(self, the_input):
        """Completion for /nick"""
        nicks = [os.environ.get('USER'), config.get('default_nick', ''), self.core.get_bookmark_nickname(self.get_name())]
mathieui's avatar
mathieui committed
861
        nicks = [i for i in nicks if i]
862
        return the_input.auto_completion(nicks, '', quotify=False)
mathieui's avatar
mathieui committed
863

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

mathieui's avatar
mathieui committed
869
    def completion_ignore(self, the_input):
mathieui's avatar
mathieui committed
870
        """Completion for /ignore"""
mathieui's avatar
mathieui committed
871
        userlist = [user.nick for user in self.users]
872
873
874
        if self.own_nick in userlist:
            userlist.remove(self.own_nick)
        userlist.sort()
875
        return the_input.auto_completion(userlist, quotify=False)
mathieui's avatar
mathieui committed
876

mathieui's avatar
mathieui committed
877
878
    def completion_role(self, the_input):
        """Completion for /role"""
879
880
        n = the_input.get_argument_position(quoted=True)
        if n == 1:
mathieui's avatar
mathieui committed
881
            userlist = [user.nick for user in self.users]
882
883
            if self.own_nick in userlist:
                userlist.remove(self.own_nick)
884
885
            return the_input.new_completion(userlist, 1, '', quotify=True)
        elif n == 2:
mathieui's avatar
mathieui committed
886
            possible_roles = ['none', 'visitor', 'participant', 'moderator']
887
            return the_input.new_completion(possible_roles, 2, '', quotify=True)
mathieui's avatar
mathieui committed
888

mathieui's avatar
mathieui committed
889
890
    def completion_affiliation(self, the_input):
        """Completion for /affiliation"""
891
892
        n = the_input.get_argument_position(quoted=True)
        if n == 1:
mathieui's avatar
mathieui committed
893
            userlist = [user.nick for user in self.users]
894
895
            if self.own_nick in userlist:
                userlist.remove(self.own_nick)
896
            jidlist = [user.jid.bare for user in self.users]
897
898
            if self.core.xmpp.boundjid.bare in jidlist:
                jidlist.remove(self.core.xmpp.boundjid.bare)
899
            userlist.extend(jidlist)
900
901
            return the_input.new_completion(userlist, 1, '', quotify=True)
        elif n == 2:
902
            possible_affiliations = ['none', 'member', 'admin', 'owner', 'outcast']
903
            return the_input.new_completion(possible_affiliations, 2,  '', quotify=True)
mathieui's avatar
mathieui committed
904

905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
    def command_invite(self, args):
        """/invite <jid> [reason]"""
        args = common.shell_split(args)
        if len(args) == 1:
            jid, reason = args[0], ''
        elif len(args) == 2:
            jid, reason = args
        else:
            return self.core.command_help('invite')
        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)

922
923
    def scroll_user_list_up(self):
        self.user_win.scroll_up()
924
        self.user_win.refresh(self.users)
925
        self.input.refresh()
926
927
928

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

932
    def command_info(self, arg):
933
934
935
936
        """
        /info <nick>
        """
        if not arg:
mathieui's avatar
mathieui committed
937
            return self.core.command_help('info')
938
        user = self.get_user_by_name(arg)
939
        if not user:
940
            return self.core.information("Unknown user: %s" % arg)
941
942
943
944
945
946
        theme = get_theme()
        info = '\x19%s}%s\x19o%s: show: \x19%s}%s\x19o, affiliation: \x19%s}%s\x19o, role: \x19%s}%s\x19o%s' % (
                        dump_tuple(user.color),
                        arg,
                        (' (\x19%s}%s\x19o)' % (dump_tuple(theme.COLOR_MUC_JID), user.jid)) if user.jid != '' else '',
                        dump_tuple(theme.color_show(user.show)),
947
                        user.show or 'Available',
948
                        dump_tuple(theme.color_role(user.role)),
949
                        user.affiliation or 'None',
950
                        dump_tuple(theme.color_role(user.role)),
951
                        user.role or 'None',
952
                        '\n%s' % user.status if user.status else '')
953
        self.core.information(info, 'Info')
954

louiz’'s avatar
louiz’ committed
955
    def command_configure(self, arg):
mathieui's avatar
mathieui committed
956
        form = fixes.get_room_form(self.core.xmpp, self.get_name())
957
        if not form:
mathieui's avatar
mathieui committed
958
            self.core.information('Could not retrieve the configuration form', 'Error')
959
            return
louiz’'s avatar
louiz’ committed
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
        self.core.open_new_form(form, self.cancel_config, self.send_config)

    def cancel_config(self, form):
        """
        The user do not want to send his/her config, send an iq cancel
        """
        self.core.xmpp.plugin['xep_0045'].cancelConfig(self.get_name())
        self.core.close_tab()

    def send_config(self, form):
        """
        The user sends his/her config to the server
        """
        self.core.xmpp.plugin['xep_0045'].configureRoom(self.get_name(), form)
        self.core.close_tab()

976
    def command_cycle(self, arg):
mathieui's avatar
mathieui committed
977
        """/cycle [reason]"""
978
979
980
981
        if self.joined:
            muc.leave_groupchat(self.core.xmpp, self.get_name(), self.own_nick, arg)
        self.disconnect()
        self.core.disable_private_tabs(self.name)
982
        self.core.command_join('"/%s"' % self.own_nick)
mathieui's avatar
mathieui committed
983
        self.user_win.pos = 0
984

985
986
    def command_recolor(self, arg):
        """
987
        /recolor [random]
988
989
        Re-assign color to the participants of the room
        """
990
        arg = arg.strip()
991
        compare_users = lambda x: x.last_talked
992
        users = list(self.users)
993
994
995
        sorted_users = sorted(users, key=compare_users, reverse=True)
        # search our own user, to remove it from the list
        for user in sorted_users:
996
            if user.nick == self.own_nick:
997
998
                sorted_users.remove(user)
                user.color = get_theme().COLOR_OWN_NICK
louiz’'s avatar
louiz’ committed
999
        colors = list(get_theme().LIST_COLOR_NICKNAMES)
1000
        if arg and arg == 'random':
For faster browsing, not all history is shown. View entire blame