privatetab.py 15.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12
"""
Module for the PrivateTab

A PrivateTab is a private conversation opened with someone from a MUC
(see muctab.py). The conversation happens with both JID being relative
to the MUC (room@server/nick1 and room@server/nick2).

This tab references his parent room, and is modified to keep track of
both participant’s nicks. It also has slightly different features than
the ConversationTab (such as tab-completion on nicks from the room).

"""
13
import curses
14 15
import logging
from typing import Dict, Callable
16

17 18
from slixmpp import JID

mathieui's avatar
mathieui committed
19
from poezio.tabs import OneToOneTab, MucTab, Tab
20

21 22
from poezio import windows
from poezio import xhtml
23
from poezio.common import safeJID
24
from poezio.config import config
25
from poezio.core.structs import Command
26 27 28 29
from poezio.decorators import refresh_wrapper
from poezio.logger import logger
from poezio.theming import get_theme, dump_tuple
from poezio.decorators import command_args_parser
30

31 32
log = logging.getLogger(__name__)

mathieui's avatar
mathieui committed
33

34
class PrivateTab(OneToOneTab):
35
    """
36
    The tab containing a private conversation (someone from a MUC)
37
    """
38 39
    plugin_commands = {}  # type: Dict[str, Command]
    plugin_keys = {}  # type: Dict[str, Callable]
40
    message_type = 'chat'
41
    additional_information = {}  # type: Dict[str, Callable[[str], str]]
mathieui's avatar
mathieui committed
42

43
    def __init__(self, core, name, nick):
44
        OneToOneTab.__init__(self, core, name)
45
        self.own_nick = nick
46
        self.name = name
47 48 49 50 51 52 53
        self.text_win = windows.TextWin()
        self._text_buffer.add_window(self.text_win)
        self.info_header = windows.PrivateInfoWin()
        self.input = windows.MessageInput()
        # keys
        self.key_func['^I'] = self.completion
        # commands
mathieui's avatar
mathieui committed
54 55 56 57
        self.register_command(
            'info',
            self.command_info,
            desc=
Kim Alvefur's avatar
Kim Alvefur committed
58
            'Display some information about the user in the MUC: their role, affiliation, status and status message.',
mathieui's avatar
mathieui committed
59 60 61 62 63 64 65
            shortdesc='Info about the user.')
        self.register_command(
            'version',
            self.command_version,
            desc=
            'Get the software version of the current interlocutor (usually its XMPP client and Operating System).',
            shortdesc='Get the software version of a jid.')
66
        self.resize()
67
        self.parent_muc = self.core.tabs.by_name_and_class(
68
            safeJID(name).bare, MucTab)
69 70 71 72
        self.on = True
        self.update_commands()
        self.update_keys()

73
    def remote_user_color(self):
74
        user = self.parent_muc.get_user_by_name(safeJID(self.name).resource)
75
        if user:
mathieui's avatar
mathieui committed
76
            return dump_tuple(user.color)
77 78
        return super().remote_user_color()

79 80
    @property
    def general_jid(self):
81
        return self.name
82

83 84 85
    def get_dest_jid(self):
        return self.name

86 87 88 89
    @property
    def nick(self):
        return self.get_nick()

90 91 92 93 94 95
    def ack_message(self, msg_id: str, msg_jid: JID):
        # special case when talking to oneself
        if msg_jid == self.core.xmpp.boundjid:
            msg_jid = JID(self.name)
        super().ack_message(msg_id, msg_jid)

96
    @staticmethod
97
    @refresh_wrapper.always
98 99 100 101
    def add_information_element(plugin_name, callback):
        """
        Lets a plugin add its own information to the PrivateInfoWin
        """
102
        PrivateTab.additional_information[plugin_name] = callback
103 104

    @staticmethod
105
    @refresh_wrapper.always
106
    def remove_information_element(plugin_name):
107
        del PrivateTab.additional_information[plugin_name]
108 109

    def load_logs(self, log_nb):
110 111 112
        logs = logger.get_logs(
            safeJID(self.name).full.replace('/', '\\'), log_nb)
        return logs
113 114 115 116 117

    def log_message(self, txt, nickname, time=None, typ=1):
        """
        Log the messages in the archives.
        """
mathieui's avatar
mathieui committed
118 119
        if not logger.log_message(
                self.name, nickname, txt, date=time, typ=typ):
120
            self.core.information('Unable to write in the log file', 'Error')
121 122

    def on_close(self):
123
        super().on_close()
124 125 126 127 128 129 130 131 132 133 134 135 136
        self.parent_muc.privates.remove(self)

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

        # If we are not completing a command or a command's argument, complete a nick
        compare_users = lambda x: x.last_talked
        word_list = [user.nick for user in sorted(self.parent_muc.users, key=compare_users, reverse=True)\
                         if user.nick != self.own_nick]
137
        after = config.get('after_completion') + ' '
138 139 140 141 142 143 144
        input_pos = self.input.pos
        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):
            add_after = after
        else:
            add_after = ''
        self.input.auto_completion(word_list, add_after, quotify=False)
mathieui's avatar
mathieui committed
145 146 147
        empty_after = self.input.get_text() == '' or (
            self.input.get_text().startswith('/')
            and not self.input.get_text().startswith('//'))
148 149
        self.send_composing_chat_state(empty_after)

150
    @command_args_parser.raw
151 152 153
    def command_say(self, line, attention=False, correct=False):
        if not self.on:
            return
154
        echo_message = JID(self.name).resource != self.own_nick
155
        msg = self.core.xmpp.make_message(self.name)
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
        msg['type'] = 'chat'
        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('private_say', msg, self)
        if not msg['body']:
            self.cancel_paused_delay()
            self.text_win.refresh()
            self.input.refresh()
            return
        user = self.parent_muc.get_user_by_name(self.own_nick)
        replaced = False
        if correct or msg['replace']['id']:
            msg['replace']['id'] = self.last_sent_message['id']
171 172
            if (config.get_by_tabname('group_corrections', self.name)
                    and echo_message):
173
                try:
mathieui's avatar
mathieui committed
174 175 176 177 178 179 180
                    self.modify_message(
                        msg['body'],
                        self.last_sent_message['id'],
                        msg['id'],
                        user=user,
                        jid=self.core.xmpp.boundjid,
                        nickname=self.own_nick)
181 182 183 184 185 186 187 188 189 190
                    replaced = True
                except:
                    log.error('Unable to correct a message', exc_info=True)
        else:
            del msg['replace']

        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'])
191
        if config.get_by_tabname('send_chat_states', self.general_jid):
192 193
            needed = 'inactive' if self.inactive else 'active'
            msg['chat_state'] = needed
194
        if attention:
195 196 197 198 199 200 201
            msg['attention'] = True
        self.core.events.trigger('private_say_after', msg, self)
        if not msg['body']:
            self.cancel_paused_delay()
            self.text_win.refresh()
            self.input.refresh()
            return
202
        if not replaced and echo_message:
mathieui's avatar
mathieui committed
203 204 205 206 207 208 209 210
            self.add_message(
                msg['body'],
                nickname=self.own_nick or self.core.own_nick,
                forced_user=user,
                nick_color=get_theme().COLOR_OWN_NICK,
                identifier=msg['id'],
                jid=self.core.xmpp.boundjid,
                typ=1)
211 212

        self.last_sent_message = msg
213
        msg._add_receipt = True
214 215 216 217 218
        msg.send()
        self.cancel_paused_delay()
        self.text_win.refresh()
        self.input.refresh()

219 220
    @command_args_parser.quoted(0, 1)
    def command_version(self, args):
221 222 223
        """
        /version
        """
224
        if args:
225
            return self.core.command.version(args[0])
226
        jid = safeJID(self.name)
227 228
        self.core.xmpp.plugin['xep_0092'].get_version(
            jid, callback=self.core.handler.on_version_result)
229

230
    @command_args_parser.quoted(0, 1)
231 232 233 234
    def command_info(self, arg):
        """
        /info
        """
mathieui's avatar
mathieui committed
235 236
        if arg and arg[0]:
            self.parent_muc.command_info(arg[0])
237
        else:
238
            user = safeJID(self.name).resource
239 240 241
            self.parent_muc.command_info(user)

    def resize(self):
242 243
        self.need_resize = False

244 245 246 247 248 249 250
        if self.size.tab_degrade_y:
            info_win_height = 0
            tab_win_height = 0
        else:
            info_win_height = self.core.information_win_size
            tab_win_height = Tab.tab_win_height()

mathieui's avatar
mathieui committed
251 252 253
        self.text_win.resize(
            self.height - 2 - info_win_height - tab_win_height, self.width, 0,
            0)
254
        self.text_win.rebuild_everything(self._text_buffer)
mathieui's avatar
mathieui committed
255 256 257 258
        self.info_header.resize(
            1, self.width, self.height - 2 - info_win_height - tab_win_height,
            0)
        self.input.resize(1, self.width, self.height - 1, 0)
259 260 261 262

    def refresh(self):
        if self.need_resize:
            self.resize()
263
        log.debug('  TAB   Refresh: %s', self.__class__.__name__)
264 265
        display_info_win = not self.size.tab_degrade_y

266
        self.text_win.refresh()
267
        self.info_header.refresh(self.name, self.text_win, self.chatstate,
268
                                 PrivateTab.additional_information)
269 270 271
        if display_info_win:
            self.info_win.refresh()

272 273 274 275
        self.refresh_tab_win()
        self.input.refresh()

    def refresh_info_header(self):
mathieui's avatar
mathieui committed
276 277
        self.info_header.refresh(self.name, self.text_win, self.chatstate,
                                 PrivateTab.additional_information)
278 279 280
        self.input.refresh()

    def get_nick(self):
281
        return safeJID(self.name).resource
282 283 284 285 286 287 288 289

    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)
        if not self.on:
            return False
mathieui's avatar
mathieui committed
290 291 292
        empty_after = self.input.get_text() == '' or (
            self.input.get_text().startswith('/')
            and not self.input.get_text().startswith('//'))
293
        tab = self.core.tabs.by_name_and_class(safeJID(self.name).bare, MucTab)
294 295 296 297 298
        if tab and tab.joined:
            self.send_composing_chat_state(empty_after)
        return False

    def on_lose_focus(self):
299 300 301 302 303
        if self.input.text:
            self.state = 'nonempty'
        else:
            self.state = 'normal'

304 305
        self.text_win.remove_line_separator()
        self.text_win.add_line_separator(self._text_buffer)
306
        tab = self.core.tabs.by_name_and_class(safeJID(self.name).bare, MucTab)
mathieui's avatar
mathieui committed
307 308
        if tab and tab.joined and config.get_by_tabname(
                'send_chat_states', self.general_jid) and self.on:
309 310 311 312 313 314
            self.send_chat_state('inactive')
        self.check_scrolled()

    def on_gain_focus(self):
        self.state = 'current'
        curses.curs_set(1)
315
        tab = self.core.tabs.by_name_and_class(safeJID(self.name).bare, MucTab)
mathieui's avatar
mathieui committed
316 317 318 319
        if tab and tab.joined and config.get_by_tabname(
                'send_chat_states',
                self.general_jid,
        ) and not self.input.get_text() and self.on:
320 321 322
            self.send_chat_state('active')

    def on_info_win_size_changed(self):
mathieui's avatar
mathieui committed
323
        if self.core.information_win_size >= self.height - 3:
324
            return
mathieui's avatar
mathieui committed
325 326 327
        self.text_win.resize(
            self.height - 2 - self.core.information_win_size -
            Tab.tab_win_height(), self.width, 0, 0)
mathieui's avatar
mathieui committed
328 329 330
        self.info_header.resize(
            1, self.width, self.height - 2 - self.core.information_win_size -
            Tab.tab_win_height(), 0)
331 332 333 334

    def get_text_window(self):
        return self.text_win

335
    @refresh_wrapper.conditional
336
    def rename_user(self, old_nick, user):
337 338 339 340
        """
        The user changed her nick in the corresponding muc: update the tab’s name and
        display a message.
        """
mathieui's avatar
mathieui committed
341 342 343 344 345 346 347 348 349
        self.add_message(
            '\x19%(nick_col)s}%(old)s\x19%(info_col)s} is now '
            'known as \x19%(nick_col)s}%(new)s' % {
                'old': old_nick,
                'new': user.nick,
                'nick_col': dump_tuple(user.color),
                'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT)
            },
            typ=2)
350
        new_jid = safeJID(self.name).bare + '/' + user.nick
351
        self.name = new_jid
352
        return self.core.tabs.current_tab is self
353 354

    @refresh_wrapper.conditional
355
    def user_left(self, status_message, user):
356 357 358 359
        """
        The user left the associated MUC
        """
        self.deactivate()
360
        theme = get_theme()
mathieui's avatar
mathieui committed
361 362
        if config.get_by_tabname('display_user_color_in_join_part',
                                 self.general_jid):
363 364
            color = dump_tuple(user.color)
        else:
365
            color = dump_tuple(theme.COLOR_REMOTE_USER)
366

367
        if not status_message:
mathieui's avatar
mathieui committed
368 369 370 371
            self.add_message(
                '\x19%(quit_col)s}%(spec)s \x19%(nick_col)s}'
                '%(nick)s\x19%(info_col)s} has left the room' % {
                    'nick': user.nick,
372
                    'spec': theme.CHAR_QUIT,
mathieui's avatar
mathieui committed
373
                    'nick_col': color,
374 375
                    'quit_col': dump_tuple(theme.COLOR_QUIT_CHAR),
                    'info_col': dump_tuple(theme.COLOR_INFORMATION_TEXT)
mathieui's avatar
mathieui committed
376 377
                },
                typ=2)
378
        else:
mathieui's avatar
mathieui committed
379 380 381 382 383 384
            self.add_message(
                '\x19%(quit_col)s}%(spec)s \x19%(nick_col)s}'
                '%(nick)s\x19%(info_col)s} has left the room'
                ' (%(status)s)' % {
                    'status': status_message,
                    'nick': user.nick,
385
                    'spec': theme.CHAR_QUIT,
mathieui's avatar
mathieui committed
386
                    'nick_col': color,
387 388
                    'quit_col': dump_tuple(theme.COLOR_QUIT_CHAR),
                    'info_col': dump_tuple(theme.COLOR_INFORMATION_TEXT)
mathieui's avatar
mathieui committed
389 390
                },
                typ=2)
391
        return self.core.tabs.current_tab is self
392 393 394 395 396 397 398

    @refresh_wrapper.conditional
    def user_rejoined(self, nick):
        """
        The user (or at least someone with the same nick) came back in the MUC
        """
        self.activate()
399
        self.check_features()
400
        tab = self.parent_muc
401 402
        theme = get_theme()
        color = dump_tuple(theme.COLOR_REMOTE_USER)
403 404
        if tab and config.get_by_tabname('display_user_color_in_join_part',
                                         self.general_jid):
405 406 407
            user = tab.get_user_by_name(nick)
            if user:
                color = dump_tuple(user.color)
mathieui's avatar
mathieui committed
408 409 410 411 412
        self.add_message(
            '\x19%(join_col)s}%(spec)s \x19%(color)s}%(nick)s\x19'
            '%(info_col)s} joined the room' % {
                'nick': nick,
                'color': color,
413 414 415
                'spec': theme.CHAR_JOIN,
                'join_col': dump_tuple(theme.COLOR_JOIN_CHAR),
                'info_col': dump_tuple(theme.COLOR_INFORMATION_TEXT)
mathieui's avatar
mathieui committed
416 417
            },
            typ=2)
418
        return self.core.tabs.current_tab is self
419 420 421 422 423 424 425 426 427 428 429 430

    def activate(self, reason=None):
        self.on = True
        if reason:
            self.add_message(txt=reason, typ=2)

    def deactivate(self, reason=None):
        self.on = False
        if reason:
            self.add_message(txt=reason, typ=2)

    def matching_names(self):
431
        return [(3, safeJID(self.name).resource), (4, self.name)]
432

433
    def add_error(self, error_message):
434 435
        theme = get_theme()
        error = '\x19%s}%s\x19o' % (dump_tuple(theme.COLOR_CHAR_NACK),
436
                                    error_message)
mathieui's avatar
mathieui committed
437 438 439 440
        self.add_message(
            error,
            highlight=True,
            nickname='Error',
441
            nick_color=theme.COLOR_ERROR_MSG,
mathieui's avatar
mathieui committed
442
            typ=2)
443
        self.core.refresh_window()