privatetab.py 14.3 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 23
from poezio import windows
from poezio import xhtml
from poezio.config import config
24
from poezio.core.structs import Command
25 26 27 28
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
29

30 31
log = logging.getLogger(__name__)

mathieui's avatar
mathieui committed
32

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

42 43
    def __init__(self, core, jid, nick):
        OneToOneTab.__init__(self, core, jid)
44 45 46 47 48 49 50 51
        self.own_nick = nick
        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
52 53 54 55
        self.register_command(
            'info',
            self.command_info,
            desc=
Kim Alvefur's avatar
Kim Alvefur committed
56
            'Display some information about the user in the MUC: their role, affiliation, status and status message.',
mathieui's avatar
mathieui committed
57 58 59 60 61 62 63
            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.')
64
        self.resize()
65
        self.parent_muc = self.core.tabs.by_name_and_class(self.jid.bare, MucTab)
66 67 68 69
        self.on = True
        self.update_commands()
        self.update_keys()

70
    def remote_user_color(self):
71
        user = self.parent_muc.get_user_by_name(self.jid.resource)
72
        if user:
mathieui's avatar
mathieui committed
73
            return dump_tuple(user.color)
74 75
        return super().remote_user_color()

76 77
    @property
    def general_jid(self):
78
        return self.jid
79

80
    def get_dest_jid(self):
81
        return self.jid
82

83 84 85 86
    @property
    def nick(self):
        return self.get_nick()

87
    def ack_message(self, msg_id: str, msg_jid: JID):
mathieui's avatar
mathieui committed
88 89 90
        if JID(msg_jid).bare == self.core.xmpp.boundjid.bare:
            msg_jid = JID(self.jid.bare)
            msg_jid.resource = self.own_nick
91 92
        super().ack_message(msg_id, msg_jid)

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

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

    def log_message(self, txt, nickname, time=None, typ=1):
        """
        Log the messages in the archives.
        """
mathieui's avatar
mathieui committed
110
        if not logger.log_message(
111
                self.jid.full, nickname, txt, date=time, typ=typ):
112
            self.core.information('Unable to write in the log file', 'Error')
113 114

    def on_close(self):
115
        super().on_close()
116 117 118 119 120 121 122 123 124 125 126 127 128
        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]
129
        after = config.get('after_completion') + ' '
130 131 132 133 134 135 136
        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
137 138 139
        empty_after = self.input.get_text() == '' or (
            self.input.get_text().startswith('/')
            and not self.input.get_text().startswith('//'))
140 141
        self.send_composing_chat_state(empty_after)

142
    @refresh_wrapper.always
143
    @command_args_parser.raw
144 145 146
    def command_say(self, line, attention=False, correct=False):
        if not self.on:
            return
mathieui's avatar
mathieui committed
147 148
        our_jid = JID(self.jid.bare)
        our_jid.resource = self.own_nick
149 150
        msg = self.core.xmpp.make_message(
            mto=self.jid.full,
mathieui's avatar
mathieui committed
151
            mfrom=our_jid,
152
        )
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
        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']:
            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']
        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'])
172
        if config.get_by_tabname('send_chat_states', self.general_jid):
173 174
            needed = 'inactive' if self.inactive else 'active'
            msg['chat_state'] = needed
175
        if attention:
176 177 178 179
            msg['attention'] = True
        self.core.events.trigger('private_say_after', msg, self)
        if not msg['body']:
            return
Maxime Buquet's avatar
Maxime Buquet committed
180
        self.set_last_sent_message(msg, correct=correct)
mathieui's avatar
mathieui committed
181
        self.core.handler.on_groupchat_private_message(msg, sent=True)
182
        msg._add_receipt = True
183 184 185
        msg.send()
        self.cancel_paused_delay()

186 187
    @command_args_parser.quoted(0, 1)
    def command_version(self, args):
188 189 190
        """
        /version
        """
191
        if args:
192
            return self.core.command.version(args[0])
193
        jid = self.jid.full
194 195
        self.core.xmpp.plugin['xep_0092'].get_version(
            jid, callback=self.core.handler.on_version_result)
196

197
    @command_args_parser.quoted(0, 1)
198 199 200 201
    def command_info(self, arg):
        """
        /info
        """
mathieui's avatar
mathieui committed
202 203
        if arg and arg[0]:
            self.parent_muc.command_info(arg[0])
204
        else:
205
            user = self.jid.resource
206 207 208
            self.parent_muc.command_info(user)

    def resize(self):
209 210
        self.need_resize = False

211 212 213 214 215 216 217
        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
218 219 220
        self.text_win.resize(
            self.height - 2 - info_win_height - tab_win_height, self.width, 0,
            0)
221
        self.text_win.rebuild_everything(self._text_buffer)
mathieui's avatar
mathieui committed
222 223 224 225
        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)
226 227 228 229

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

233
        self.text_win.refresh()
234
        self.info_header.refresh(self.jid.full, self.text_win, self.chatstate,
235
                                 PrivateTab.additional_information)
236 237 238
        if display_info_win:
            self.info_win.refresh()

239 240 241 242
        self.refresh_tab_win()
        self.input.refresh()

    def refresh_info_header(self):
243
        self.info_header.refresh(self.jid.full, self.text_win, self.chatstate,
mathieui's avatar
mathieui committed
244
                                 PrivateTab.additional_information)
245 246 247
        self.input.refresh()

    def get_nick(self):
248
        return self.jid.resource
249 250 251 252 253 254 255 256

    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
257 258 259
        empty_after = self.input.get_text() == '' or (
            self.input.get_text().startswith('/')
            and not self.input.get_text().startswith('//'))
260
        tab = self.core.tabs.by_name_and_class(self.jid.bare, MucTab)
261 262 263 264 265
        if tab and tab.joined:
            self.send_composing_chat_state(empty_after)
        return False

    def on_lose_focus(self):
266 267 268 269 270
        if self.input.text:
            self.state = 'nonempty'
        else:
            self.state = 'normal'

271 272
        self.text_win.remove_line_separator()
        self.text_win.add_line_separator(self._text_buffer)
273
        tab = self.core.tabs.by_name_and_class(self.jid.bare, MucTab)
mathieui's avatar
mathieui committed
274 275
        if tab and tab.joined and config.get_by_tabname(
                'send_chat_states', self.general_jid) and self.on:
276 277 278 279 280 281
            self.send_chat_state('inactive')
        self.check_scrolled()

    def on_gain_focus(self):
        self.state = 'current'
        curses.curs_set(1)
282
        tab = self.core.tabs.by_name_and_class(self.jid.bare, MucTab)
mathieui's avatar
mathieui committed
283 284 285 286
        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:
287 288 289
            self.send_chat_state('active')

    def on_info_win_size_changed(self):
mathieui's avatar
mathieui committed
290
        if self.core.information_win_size >= self.height - 3:
291
            return
mathieui's avatar
mathieui committed
292 293 294
        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
295 296 297
        self.info_header.resize(
            1, self.width, self.height - 2 - self.core.information_win_size -
            Tab.tab_win_height(), 0)
298 299 300 301

    def get_text_window(self):
        return self.text_win

302
    @refresh_wrapper.conditional
303
    def rename_user(self, old_nick, user):
304 305 306 307
        """
        The user changed her nick in the corresponding muc: update the tab’s name and
        display a message.
        """
mathieui's avatar
mathieui committed
308 309 310 311 312 313 314 315 316
        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)
317
        new_jid = self.jid.bare + '/' + user.nick
318
        self.name = new_jid
319
        return self.core.tabs.current_tab is self
320 321

    @refresh_wrapper.conditional
322
    def user_left(self, status_message, user):
323 324 325 326
        """
        The user left the associated MUC
        """
        self.deactivate()
327
        theme = get_theme()
mathieui's avatar
mathieui committed
328 329
        if config.get_by_tabname('display_user_color_in_join_part',
                                 self.general_jid):
330 331
            color = dump_tuple(user.color)
        else:
332
            color = dump_tuple(theme.COLOR_REMOTE_USER)
333

334
        if not status_message:
mathieui's avatar
mathieui committed
335 336 337 338
            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,
339
                    'spec': theme.CHAR_QUIT,
mathieui's avatar
mathieui committed
340
                    'nick_col': color,
341 342
                    'quit_col': dump_tuple(theme.COLOR_QUIT_CHAR),
                    'info_col': dump_tuple(theme.COLOR_INFORMATION_TEXT)
mathieui's avatar
mathieui committed
343 344
                },
                typ=2)
345
        else:
mathieui's avatar
mathieui committed
346 347 348 349 350 351
            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,
352
                    'spec': theme.CHAR_QUIT,
mathieui's avatar
mathieui committed
353
                    'nick_col': color,
354 355
                    'quit_col': dump_tuple(theme.COLOR_QUIT_CHAR),
                    'info_col': dump_tuple(theme.COLOR_INFORMATION_TEXT)
mathieui's avatar
mathieui committed
356 357
                },
                typ=2)
358
        return self.core.tabs.current_tab is self
359 360 361 362 363 364 365

    @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()
366
        self.check_features()
367
        tab = self.parent_muc
368 369
        theme = get_theme()
        color = dump_tuple(theme.COLOR_REMOTE_USER)
370 371
        if tab and config.get_by_tabname('display_user_color_in_join_part',
                                         self.general_jid):
372 373 374
            user = tab.get_user_by_name(nick)
            if user:
                color = dump_tuple(user.color)
mathieui's avatar
mathieui committed
375 376 377 378 379
        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,
380 381 382
                '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
383 384
            },
            typ=2)
385
        return self.core.tabs.current_tab is self
386 387 388 389 390 391 392 393 394 395 396 397

    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):
398
        return [(3, self.jid.resource), (4, self.name)]
399

400
    def add_error(self, error_message):
401 402
        theme = get_theme()
        error = '\x19%s}%s\x19o' % (dump_tuple(theme.COLOR_CHAR_NACK),
403
                                    error_message)
mathieui's avatar
mathieui committed
404 405 406 407
        self.add_message(
            error,
            highlight=True,
            nickname='Error',
408
            nick_color=theme.COLOR_ERROR_MSG,
mathieui's avatar
mathieui committed
409
            typ=2)
410
        self.core.refresh_window()