privatetab.py 14.5 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
from xml.etree import cElementTree as ET
17

18 19
from slixmpp import JID

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

22 23 24
from poezio import windows
from poezio import xhtml
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__)

Maxime Buquet's avatar
Maxime Buquet committed
33 34
NS_MUC_USER = 'http://jabber.org/protocol/muc#user'

mathieui's avatar
mathieui committed
35

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

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

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

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

90
    def ack_message(self, msg_id: str, msg_jid: JID):
mathieui's avatar
mathieui committed
91 92 93
        if JID(msg_jid).bare == self.core.xmpp.boundjid.bare:
            msg_jid = JID(self.jid.bare)
            msg_jid.resource = self.own_nick
94 95
        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 110 111 112

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

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

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

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

202
    @command_args_parser.quoted(0, 1)
203 204 205 206
    def command_info(self, arg):
        """
        /info
        """
mathieui's avatar
mathieui committed
207 208
        if arg and arg[0]:
            self.parent_muc.command_info(arg[0])
209
        else:
210
            user = self.jid.resource
211 212 213
            self.parent_muc.command_info(user)

    def resize(self):
214 215
        self.need_resize = False

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

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

238
        self.text_win.refresh()
239
        self.info_header.refresh(self.jid.full, self.text_win, self.chatstate,
240
                                 PrivateTab.additional_information)
241 242 243
        if display_info_win:
            self.info_win.refresh()

244 245 246 247
        self.refresh_tab_win()
        self.input.refresh()

    def refresh_info_header(self):
248
        self.info_header.refresh(self.jid.full, self.text_win, self.chatstate,
mathieui's avatar
mathieui committed
249
                                 PrivateTab.additional_information)
250 251 252
        self.input.refresh()

    def get_nick(self):
253
        return self.jid.resource
254 255 256 257 258 259 260 261

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

    def on_lose_focus(self):
271 272 273 274 275
        if self.input.text:
            self.state = 'nonempty'
        else:
            self.state = 'normal'

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

    def on_gain_focus(self):
        self.state = 'current'
        curses.curs_set(1)
287
        tab = self.core.tabs.by_name_and_class(self.jid.bare, MucTab)
mathieui's avatar
mathieui committed
288 289 290 291
        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:
292 293 294
            self.send_chat_state('active')

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

    def get_text_window(self):
        return self.text_win

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

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

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

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

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

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