rostertab.py 49.1 KB
Newer Older
1 2 3 4 5 6 7
"""
The RosterInfoTab is the tab showing roster info, the list of contacts,
half of it is dedicated to showing the information buffer, and a small
rectangle shows the current contact info.

This module also includes functions to match users in the roster.
"""
8
import logging
mathieui's avatar
mathieui committed
9
import base64
10 11 12
import curses
import difflib
import os
mathieui's avatar
mathieui committed
13
import ssl
14
from functools import partial
15
from os import getenv, path
16
from pathlib import Path
17
from typing import Dict, Callable
18

19 20
from poezio import common
from poezio import windows
21
from poezio.common import safeJID, shell_split
22 23 24 25 26 27
from poezio.config import config
from poezio.contact import Contact, Resource
from poezio.decorators import refresh_wrapper
from poezio.roster import RosterGroup, roster
from poezio.theming import get_theme, dump_tuple
from poezio.decorators import command_args_parser
28
from poezio.core.structs import Command, Completion
mathieui's avatar
mathieui committed
29 30
from poezio.tabs import Tab

31 32
log = logging.getLogger(__name__)

mathieui's avatar
mathieui committed
33

34 35 36 37 38 39 40 41 42 43 44
def deny_anonymous(func: Callable) -> Callable:
    def wrap(self: 'RosterInfoTab', *args, **kwargs):
        if self.core.xmpp.anon:
            return self.core.information(
                'This command is not available for anonymous accounts.',
                'Info'
            )
        return func(self, *args, **kwargs)
    return wrap


45 46
class RosterInfoTab(Tab):
    """
47
    A tab, split in two, containing the roster and infos
48
    """
49 50
    plugin_commands = {}  # type: Dict[str, Command]
    plugin_keys = {}  # type: Dict[str, Callable]
mathieui's avatar
mathieui committed
51

52 53
    def __init__(self, core):
        Tab.__init__(self, core)
54 55 56 57 58 59
        self.name = "Roster"
        self.v_separator = windows.VerticalSeparator()
        self.information_win = windows.TextWin()
        self.core.information_buffer.add_window(self.information_win)
        self.roster_win = windows.RosterWin()
        self.contact_info_win = windows.ContactInfoWin()
60
        self.avatar_win = windows.ImageWin()
mathieui's avatar
mathieui committed
61 62
        self.default_help_message = windows.HelpText(
            "Enter commands with “/”. “o”: toggle offline show")
63 64 65 66
        self.input = self.default_help_message
        self.state = 'normal'
        self.key_func['^I'] = self.completion
        self.key_func["/"] = self.on_slash
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
        # disable most of the roster features when in anonymous mode
        if not self.core.xmpp.anon:
            self.key_func[' '] = self.on_space
            self.key_func["KEY_UP"] = self.move_cursor_up
            self.key_func["KEY_DOWN"] = self.move_cursor_down
            self.key_func["M-u"] = self.move_cursor_to_next_contact
            self.key_func["M-y"] = self.move_cursor_to_prev_contact
            self.key_func["M-U"] = self.move_cursor_to_next_group
            self.key_func["M-Y"] = self.move_cursor_to_prev_group
            self.key_func["M-[1;5B"] = self.move_cursor_to_next_group
            self.key_func["M-[1;5A"] = self.move_cursor_to_prev_group
            self.key_func["l"] = self.command_last_activity
            self.key_func["o"] = self.toggle_offline_show
            self.key_func["v"] = self.get_contact_version
            self.key_func["i"] = self.show_contact_info
            self.key_func["s"] = self.start_search
            self.key_func["S"] = self.start_search_slow
            self.key_func["n"] = self.change_contact_name
85 86 87 88 89 90
        self.register_command(
            'deny',
            self.command_deny,
            usage='[jid]',
            desc='Deny your presence to the provided JID (or the '
            'selected contact in your roster), who is asking'
Kim Alvefur's avatar
Kim Alvefur committed
91
            'you to be in their roster.',
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
            shortdesc='Deny a user your presence.',
            completion=self.completion_deny)
        self.register_command(
            'accept',
            self.command_accept,
            usage='[jid]',
            desc='Allow the provided JID (or the selected contact '
            'in your roster), to see your presence.',
            shortdesc='Allow a user your presence.',
            completion=self.completion_deny)
        self.register_command(
            'add',
            self.command_add,
            usage='<jid>',
            desc='Add the specified JID to your roster, ask them to'
            ' allow you to see his presence, and allow them to'
            ' see your presence.',
            shortdesc='Add a user to your roster.')
        self.register_command(
            'name',
            self.command_name,
            usage='<jid> [name]',
            shortdesc='Set the given JID\'s name.',
            completion=self.completion_name)
        self.register_command(
            'groupadd',
            self.command_groupadd,
            usage='[<jid> <group>]|<group>',
            desc='Add the given JID or selected line to the given group.',
            shortdesc='Add a user to a group',
            completion=self.completion_groupadd)
        self.register_command(
            'groupmove',
            self.command_groupmove,
            usage='<jid> <old group> <new group>',
            desc='Move the given JID from the old group to the new group.',
            shortdesc='Move a user to another group.',
            completion=self.completion_groupmove)
        self.register_command(
            'groupremove',
            self.command_groupremove,
            usage='<jid> <group>',
            desc='Remove the given JID from the given group.',
            shortdesc='Remove a user from a group.',
            completion=self.completion_groupremove)
        self.register_command(
            'remove',
            self.command_remove,
            usage='[jid]',
            desc='Remove the specified JID from your roster. This '
            'will unsubscribe you from its presence, cancel '
            'its subscription to yours, and remove the item '
            'from your roster.',
            shortdesc='Remove a user from your roster.',
            completion=self.completion_remove)
        self.register_command(
            'export',
            self.command_export,
            usage='[/path/to/file]',
            desc='Export your contacts into /path/to/file if '
            'specified, or $HOME/poezio_contacts if not.',
            shortdesc='Export your roster to a file.',
            completion=partial(self.completion_file, 1))
        self.register_command(
            'import',
            self.command_import,
            usage='[/path/to/file]',
            desc='Import your contacts from /path/to/file if '
            'specified, or $HOME/poezio_contacts if not.',
            shortdesc='Import your roster from a file.',
            completion=partial(self.completion_file, 1))
        self.register_command(
            'password',
            self.command_password,
            usage='<password>',
            shortdesc='Change your password')
mathieui's avatar
mathieui committed
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188

        self.register_command(
            'reconnect',
            self.command_reconnect,
            desc='Disconnect from the remote server if you are '
            'currently connected and then connect to it again.',
            shortdesc='Disconnect and reconnect to the server.')
        self.register_command(
            'disconnect',
            self.command_disconnect,
            desc='Disconnect from the remote server.',
            shortdesc='Disconnect from the server.')
        self.register_command(
            'clear', self.command_clear, shortdesc='Clear the info buffer.')
        self.register_command(
            'last_activity',
            self.command_last_activity,
            usage='<jid>',
            desc='Informs you of the last activity of a JID.',
            shortdesc='Get the activity of someone.',
            completion=self.core.completion.last_activity)
189 190 191 192 193 194

        self.resize()
        self.update_commands()
        self.update_keys()

    def check_blocking(self, features):
195
        if 'urn:xmpp:blocking' in features and not self.core.xmpp.anon:
mathieui's avatar
mathieui committed
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
            self.register_command(
                'block',
                self.command_block,
                usage='[jid]',
                shortdesc='Prevent a JID from talking to you.',
                completion=self.completion_block)
            self.register_command(
                'unblock',
                self.command_unblock,
                usage='[jid]',
                shortdesc='Allow a JID to talk to you.',
                completion=self.completion_unblock)
            self.register_command(
                'list_blocks',
                self.command_list_blocks,
                shortdesc='Show the blocked contacts.')
            self.core.xmpp.del_event_handler('session_start',
                                             self.check_blocking)
            self.core.xmpp.add_event_handler('blocked_message',
                                             self.on_blocked_message)
216

mathieui's avatar
mathieui committed
217
    def check_saslexternal(self, features):
218
        if 'urn:xmpp:saslcert:1' in features and not self.core.xmpp.anon:
mathieui's avatar
mathieui committed
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
            self.register_command(
                'certs',
                self.command_certs,
                desc='List the fingerprints of certificates'
                ' which can connect to your account.',
                shortdesc='List allowed client certs.')
            self.register_command(
                'cert_add',
                self.command_cert_add,
                desc='Add a client certificate to the authorized ones. '
                'It must have an unique name and be contained in '
                'a PEM file. [management] is a boolean indicating'
                ' if a client connected using this certificate can'
                ' manage the certificates itself.',
                shortdesc='Add a client certificate.',
                usage='<name> <certificate path> [management]',
                completion=self.completion_cert_add)
            self.register_command(
                'cert_disable',
                self.command_cert_disable,
                desc='Remove a certificate from the list '
                'of allowed ones. Clients currently '
                'using this certificate will not be '
                'forcefully disconnected.',
                shortdesc='Disable a certificate',
                usage='<name>')
            self.register_command(
                'cert_revoke',
                self.command_cert_revoke,
                desc='Remove a certificate from the list '
                'of allowed ones. Clients currently '
                'using this certificate will be '
                'forcefully disconnected.',
                shortdesc='Revoke a certificate',
                usage='<name>')
            self.register_command(
                'cert_fetch',
                self.command_cert_fetch,
                desc='Retrieve a certificate with its '
                'name. It will be stored in <path>.',
                shortdesc='Fetch a certificate',
                usage='<name> <path>',
                completion=self.completion_cert_fetch)
mathieui's avatar
mathieui committed
262

263 264 265 266
    @property
    def selected_row(self):
        return self.roster_win.get_selected_row()

mathieui's avatar
mathieui committed
267 268 269 270 271
    @command_args_parser.ignored
    def command_certs(self):
        """
        /certs
        """
mathieui's avatar
mathieui committed
272

mathieui's avatar
mathieui committed
273 274
        def cb(iq):
            if iq['type'] == 'error':
mathieui's avatar
mathieui committed
275 276
                self.core.information(
                    'Unable to retrieve the certificate list.', 'Error')
mathieui's avatar
mathieui committed
277 278 279 280 281 282 283
                return
            certs = []
            for item in iq['sasl_certs']['items']:
                users = '\n'.join(item['users'])
                certs.append((item['name'], users))

            if not certs:
284 285
                return self.core.information('No certificates found', 'Info')
            msg = 'Certificates:\n'
mathieui's avatar
mathieui committed
286 287 288
            msg += '\n'.join(
                (('  %s%s' % (item[0] + (': ' if item[1] else ''), item[1]))
                 for item in certs))
mathieui's avatar
mathieui committed
289 290 291 292 293 294 295 296 297 298
            self.core.information(msg, 'Info')

        self.core.xmpp.plugin['xep_0257'].get_certs(callback=cb, timeout=3)

    @command_args_parser.quoted(2, 1)
    def command_cert_add(self, args):
        """
        /cert_add <name> <certfile> [cert-management]
        """
        if not args or len(args) < 2:
299
            return self.core.command.help('cert_add')
mathieui's avatar
mathieui committed
300

mathieui's avatar
mathieui committed
301 302
        def cb(iq):
            if iq['type'] == 'error':
mathieui's avatar
mathieui committed
303 304
                self.core.information('Unable to add the certificate.',
                                      'Error')
mathieui's avatar
mathieui committed
305
            else:
306
                self.core.information('Certificate added.', 'Info')
mathieui's avatar
mathieui committed
307 308 309 310 311 312

        name = args[0]

        try:
            with open(args[1]) as fd:
                crt = fd.read()
mathieui's avatar
mathieui committed
313 314
            crt = crt.replace(ssl.PEM_FOOTER, '').replace(
                ssl.PEM_HEADER, '').replace(' ', '').replace('\n', '')
mathieui's avatar
mathieui committed
315
        except Exception as e:
mathieui's avatar
mathieui committed
316 317
            self.core.information('Unable to read the certificate: %s' % e,
                                  'Error')
mathieui's avatar
mathieui committed
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
            return

        if len(args) > 2:
            management = args[2]
            if management:
                management = management.lower()
                if management not in ('false', '0'):
                    management = True
                else:
                    management = False
            else:
                management = False
        else:
            management = True

mathieui's avatar
mathieui committed
333 334
        self.core.xmpp.plugin['xep_0257'].add_cert(
            name, crt, callback=cb, allow_management=management)
mathieui's avatar
mathieui committed
335

336 337 338 339 340 341 342 343 344 345 346
    def completion_cert_add(self, the_input):
        """
        completion for /cert_add <name> <path> [management]
        """
        n = the_input.get_argument_position()
        log.debug('%s %s %s', the_input.text, n, the_input.pos)
        if n == 1:
            return
        elif n == 2:
            return self.completion_file(2, the_input)
        elif n == 3:
347
            return Completion(the_input.new_completion, ['true', 'false'], n)
348

mathieui's avatar
mathieui committed
349 350 351 352 353 354
    @command_args_parser.quoted(1)
    def command_cert_disable(self, args):
        """
        /cert_disable <name>
        """
        if not args:
355
            return self.core.command.help('cert_disable')
mathieui's avatar
mathieui committed
356

mathieui's avatar
mathieui committed
357 358
        def cb(iq):
            if iq['type'] == 'error':
mathieui's avatar
mathieui committed
359 360
                self.core.information('Unable to disable the certificate.',
                                      'Error')
mathieui's avatar
mathieui committed
361
            else:
362
                self.core.information('Certificate disabled.', 'Info')
mathieui's avatar
mathieui committed
363 364 365 366 367 368 369 370 371 372 373

        name = args[0]

        self.core.xmpp.plugin['xep_0257'].disable_cert(name, callback=cb)

    @command_args_parser.quoted(1)
    def command_cert_revoke(self, args):
        """
        /cert_revoke <name>
        """
        if not args:
374
            return self.core.command.help('cert_revoke')
mathieui's avatar
mathieui committed
375

mathieui's avatar
mathieui committed
376 377
        def cb(iq):
            if iq['type'] == 'error':
mathieui's avatar
mathieui committed
378 379
                self.core.information('Unable to revoke the certificate.',
                                      'Error')
mathieui's avatar
mathieui committed
380
            else:
381
                self.core.information('Certificate revoked.', 'Info')
mathieui's avatar
mathieui committed
382 383 384 385 386 387 388 389 390 391 392

        name = args[0]

        self.core.xmpp.plugin['xep_0257'].revoke_cert(name, callback=cb)

    @command_args_parser.quoted(2)
    def command_cert_fetch(self, args):
        """
        /cert_fetch <name> <path>
        """
        if not args or len(args) < 2:
393
            return self.core.command.help('cert_fetch')
mathieui's avatar
mathieui committed
394

mathieui's avatar
mathieui committed
395 396
        def cb(iq):
            if iq['type'] == 'error':
397 398
                self.core.information('Unable to fetch the certificate.',
                                      'Error')
mathieui's avatar
mathieui committed
399 400 401 402 403 404 405 406 407
                return

            cert = None
            for item in iq['sasl_certs']['items']:
                if item['name'] == name:
                    cert = base64.b64decode(item['x509cert'])
                    break

            if not cert:
408
                return self.core.information('Certificate not found.', 'Info')
mathieui's avatar
mathieui committed
409 410 411 412 413

            cert = ssl.DER_cert_to_PEM_cert(cert)
            with open(path, 'w') as fd:
                fd.write(cert)

414
            self.core.information('File stored at %s' % path, 'Info')
mathieui's avatar
mathieui committed
415 416 417 418 419 420

        name = args[0]
        path = args[1]

        self.core.xmpp.plugin['xep_0257'].get_certs(callback=cb)

421 422 423 424 425 426 427 428 429 430 431
    def completion_cert_fetch(self, the_input):
        """
        completion for /cert_fetch <name> <path>
        """
        n = the_input.get_argument_position()
        log.debug('%s %s %s', the_input.text, n, the_input.pos)
        if n == 1:
            return
        elif n == 2:
            return self.completion_file(2, the_input)

432 433 434 435 436 437
    def on_blocked_message(self, message):
        """
        When we try to send a message to a blocked contact
        """
        tab = self.core.get_conversation_by_jid(message['from'], False)
        if not tab:
mathieui's avatar
mathieui committed
438 439
            log.debug('Received message from nonexistent tab: %s',
                      message['from'])
440
        message = '\x19%(info_col)s}Cannot send message to %(jid)s: contact blocked' % {
mathieui's avatar
mathieui committed
441 442 443
            'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
            'jid': message['from'],
        }
444 445
        tab.add_message(message)

446 447
    @command_args_parser.quoted(0, 1)
    def command_block(self, args):
448 449 450 451
        """
        /block [jid]
        """
        item = self.roster_win.selected_row
452 453
        if args:
            jid = safeJID(args[0])
454 455 456 457
        elif isinstance(item, Contact):
            jid = item.bare_jid
        elif isinstance(item, Resource):
            jid = item.jid.bare
458 459 460

        def callback(iq):
            if iq['type'] == 'error':
mathieui's avatar
mathieui committed
461 462
                return self.core.information('Could not block %s.' % jid,
                                             'Error')
463 464 465
            elif iq['type'] == 'result':
                return self.core.information('Blocked %s.' % jid, 'Info')

466
        self.core.xmpp.plugin['xep_0191'].block(jid, callback=callback)
467 468 469 470 471 472 473

    def completion_block(self, the_input):
        """
        Completion for /block
        """
        if the_input.get_argument_position() == 1:
            jids = roster.jids()
mathieui's avatar
mathieui committed
474 475
            return Completion(
                the_input.new_completion, jids, 1, '', quotify=False)
476

477 478
    @command_args_parser.quoted(0, 1)
    def command_unblock(self, args):
479 480 481
        """
        /unblock [jid]
        """
mathieui's avatar
mathieui committed
482

483 484
        def callback(iq):
            if iq['type'] == 'error':
mathieui's avatar
mathieui committed
485 486
                return self.core.information('Could not unblock the contact.',
                                             'Error')
487 488 489 490
            elif iq['type'] == 'result':
                return self.core.information('Contact unblocked.', 'Info')

        item = self.roster_win.selected_row
491 492
        if args:
            jid = safeJID(args[0])
493 494 495 496
        elif isinstance(item, Contact):
            jid = item.bare_jid
        elif isinstance(item, Resource):
            jid = item.jid.bare
497
        self.core.xmpp.plugin['xep_0191'].unblock(jid, callback=callback)
498 499 500 501 502

    def completion_unblock(self, the_input):
        """
        Completion for /unblock
        """
mathieui's avatar
mathieui committed
503

504 505 506 507
        def on_result(iq):
            if iq['type'] == 'error':
                return
            l = sorted(str(item) for item in iq['blocklist']['items'])
508
            return Completion(the_input.new_completion, l, 1, quotify=False)
509

510
        if the_input.get_argument_position():
511 512
            self.core.xmpp.plugin['xep_0191'].get_blocked(callback=on_result)
        return True
513

514 515
    @command_args_parser.ignored
    def command_list_blocks(self):
516 517 518
        """
        /list_blocks
        """
mathieui's avatar
mathieui committed
519

520 521
        def callback(iq):
            if iq['type'] == 'error':
mathieui's avatar
mathieui committed
522 523
                return self.core.information(
                    'Could not retrieve the blocklist.', 'Error')
524 525 526 527 528 529 530 531 532
            s = 'List of blocked JIDs:\n'
            items = (str(item) for item in iq['blocklist']['items'])
            jids = '\n'.join(items)
            if jids:
                s += jids
            else:
                s = 'No blocked JIDs.'
            self.core.information(s, 'Info')

533
        self.core.xmpp.plugin['xep_0191'].get_blocked(callback=callback)
534

535 536
    @command_args_parser.ignored
    def command_reconnect(self):
537 538 539
        """
        /reconnect
        """
540 541 542 543
        if self.core.xmpp.is_connected():
            self.core.disconnect(reconnect=True)
        else:
            self.core.xmpp.connect()
544

545 546
    @command_args_parser.ignored
    def command_disconnect(self):
547 548 549 550 551
        """
        /disconnect
        """
        self.core.disconnect()

552 553
    @command_args_parser.quoted(0, 1)
    def command_last_activity(self, args):
554 555 556 557
        """
        /activity [jid]
        """
        item = self.roster_win.selected_row
558 559
        if args:
            jid = args[0]
560 561 562
        elif isinstance(item, Contact):
            jid = item.bare_jid
        elif isinstance(item, Resource):
563
            jid = item.jid
564 565 566
        else:
            self.core.information('No JID selected.', 'Error')
            return
567
        self.core.command.last_activity(jid)
568 569

    def resize(self):
570
        self.need_resize = False
571 572 573 574 575 576 577 578 579 580 581
        if self.size.tab_degrade_x:
            display_info = False
            roster_width = self.width
        else:
            display_info = True
            roster_width = self.width // 2
            if self.size.tab_degrade_y:
                display_contact_win = False
                contact_win_h = 0
            else:
                display_contact_win = True
582
                contact_win_h = 8
583 584 585 586 587 588 589
        if self.size.tab_degrade_y:
            tab_win_height = 0
        else:
            tab_win_height = Tab.tab_win_height()

        info_width = self.width - roster_width - 1
        if display_info:
mathieui's avatar
mathieui committed
590 591 592 593 594
            self.v_separator.resize(self.height - 1 - tab_win_height, 1, 0,
                                    roster_width)
            self.information_win.resize(
                self.height - 1 - tab_win_height - contact_win_h, info_width,
                0, roster_width + 1, self.core.information_buffer)
595
            if display_contact_win:
596 597
                y = self.height - tab_win_height - contact_win_h - 1
                avatar_width = contact_win_h * 2
598
                self.contact_info_win.resize(contact_win_h,
mathieui's avatar
mathieui committed
599 600 601 602
                                             info_width - avatar_width, y,
                                             roster_width + 1)
                self.avatar_win.resize(contact_win_h, avatar_width, y,
                                       self.width - avatar_width)
603 604
        self.roster_win.resize(self.height - 1 - Tab.tab_win_height(),
                               roster_width, 0, 0)
mathieui's avatar
mathieui committed
605 606
        self.input.resize(1, self.width, self.height - 1, 0)
        self.default_help_message.resize(1, self.width, self.height - 1, 0)
607 608 609 610 611 612 613

    def completion(self):
        # Check if we are entering a command (with the '/' key)
        if isinstance(self.input, windows.Input) and\
                not self.input.help_message:
            self.complete_commands(self.input)

614
    def completion_file(self, complete_number, the_input):
615
        """
616 617 618
        Generic quoted completion for files/paths
        (use functools.partial to use directly as a completion
        for a command)
619 620
        """
        text = the_input.get_text()
621
        args = shell_split(text)
622 623
        n = the_input.get_argument_position()
        if n == complete_number:
mathieui's avatar
mathieui committed
624
            if args[n - 1] == '' or len(args) < n + 1:
625
                home = os.getenv('HOME', default='/')
mathieui's avatar
mathieui committed
626 627
                return Completion(
                    the_input.new_completion, [home, '/tmp'], n, quotify=True)
628 629
            path_ = Path(args[n])
            if path_.is_dir():
630 631 632
                dir_ = path_
                base = ''
            else:
633 634
                dir_ = path_.parent
                base = path_.name
635
            try:
636
                names = list(dir_.iterdir())
637
            except OSError:
638
                names = []
mathieui's avatar
mathieui committed
639 640 641
            names_filtered = [
                name for name in names if str(name).startswith(base)
            ]
642 643 644 645
            if names_filtered:
                names = names_filtered
            if not names:
                names = [path_]
646 647
            end_list = []
            for name in names:
648 649 650
                if not str(name).startswith('.'):
                    value = dir_ / name
                    end_list.append(str(value))
651

mathieui's avatar
mathieui committed
652 653
            return Completion(
                the_input.new_completion, end_list, n, quotify=True)
654

655 656
    @command_args_parser.ignored
    def command_clear(self):
657 658 659 660 661
        """
        /clear
        """
        self.core.information_buffer.messages = []
        self.information_win.rebuild_everything(self.core.information_buffer)
mathieui's avatar
mathieui committed
662 663
        self.core.information_win.rebuild_everything(
            self.core.information_buffer)
664 665
        self.refresh()

666
    @deny_anonymous
667 668
    @command_args_parser.quoted(1)
    def command_password(self, args):
669 670 671
        """
        /password <password>
        """
mathieui's avatar
mathieui committed
672

673 674 675
        def callback(iq):
            if iq['type'] == 'result':
                self.core.information('Password updated', 'Account')
676
                if config.get('password'):
677
                    config.silent_set('password', args[0])
678
            else:
mathieui's avatar
mathieui committed
679 680 681 682 683
                self.core.information('Unable to change the password',
                                      'Account')

        self.core.xmpp.plugin['xep_0077'].change_password(
            args[0], callback=callback)
684

685
    @deny_anonymous
686 687
    @command_args_parser.quoted(0, 1)
    def command_deny(self, args):
688 689 690 691
        """
        /deny [jid]
        Denies a JID from our roster
        """
692
        if not args:
693 694 695 696
            item = self.roster_win.selected_row
            if isinstance(item, Contact):
                jid = item.bare_jid
            else:
697
                self.core.information('No subscription to deny', 'Warning')
698 699
                return
        else:
700
            jid = safeJID(args[0]).bare
701
            if jid not in [jid for jid in roster.jids()]:
702
                self.core.information('No subscription to deny', 'Warning')
703 704 705 706 707
                return

        contact = roster[jid]
        if contact:
            contact.unauthorize()
708 709
            self.core.information('Subscription to %s was revoked' % jid,
                                  'Roster')
710

711
    @deny_anonymous
712
    @command_args_parser.quoted(1)
713 714
    def command_add(self, args):
        """
Link Mauve's avatar
Link Mauve committed
715
        Add the specified JID to the roster, and automatically
716 717
        accept the reverse subscription
        """
718
        if args is None:
719
            self.core.information('No JID specified', 'Error')
720 721 722
            return
        jid = safeJID(safeJID(args[0]).bare)
        if not str(jid):
mathieui's avatar
mathieui committed
723 724
            self.core.information(
                'The provided JID (%s) is not valid' % (args[0], ), 'Error')
725 726 727 728 729
            return
        if jid in roster and roster[jid].subscription in ('to', 'both'):
            return self.core.information('Already subscribed.', 'Roster')
        roster.add(jid)
        roster.modified()
730
        self.core.information('%s was added to the roster' % jid, 'Roster')
731

732
    @deny_anonymous
louiz’'s avatar
louiz’ committed
733
    @command_args_parser.quoted(1, 1)
734
    def command_name(self, args):
735 736 737
        """
        Set a name for the specified JID in your roster
        """
mathieui's avatar
mathieui committed
738

739 740 741 742
        def callback(iq):
            if not iq:
                self.core.information('The name could not be set.', 'Error')
                log.debug('Error in /name:\n%s', iq)
mathieui's avatar
mathieui committed
743

louiz’'s avatar
louiz’ committed
744
        if args is None:
745
            return self.core.command.help('name')
746 747 748 749 750
        jid = safeJID(args[0]).bare
        name = args[1] if len(args) == 2 else ''

        contact = roster[jid]
        if contact is None:
751
            self.core.information('No such JID in roster', 'Error')
752 753 754 755 756 757
            return

        groups = set(contact.groups)
        if 'none' in groups:
            groups.remove('none')
        subscription = contact.subscription
mathieui's avatar
mathieui committed
758 759 760 761 762 763
        self.core.xmpp.update_roster(
            jid,
            name=name,
            groups=groups,
            subscription=subscription,
            callback=callback)
764

765
    @deny_anonymous
766
    @command_args_parser.quoted(1, 1)
767 768 769 770
    def command_groupadd(self, args):
        """
        Add the specified JID to the specified group
        """
771
        if args is None:
772
            return self.core.command.help('groupadd')
773 774 775 776 777 778 779 780 781 782 783 784
        if len(args) == 1:
            group = args[0]
            item = self.roster_win.selected_row
            if isinstance(item, Contact):
                jid = item.bare_jid
            elif isinstance(item, Resource):
                jid = item.jid
            else:
                return self.core.command.help('groupadd')
        else:
            jid = safeJID(args[0]).bare
            group = args[1]
785 786 787

        contact = roster[jid]
        if contact is None:
788
            self.core.information('No such JID in roster', 'Error')
789 790 791 792
            return

        new_groups = set(contact.groups)
        if group in new_groups:
793
            self.core.information('JID already in group', 'Error')
794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812
            return

        roster.modified()
        new_groups.add(group)
        try:
            new_groups.remove('none')
        except KeyError:
            pass

        name = contact.name
        subscription = contact.subscription

        def callback(iq):
            if iq:
                roster.update_contact_groups(jid)
            else:
                self.core.information('The group could not be set.', 'Error')
                log.debug('Error in groupadd:\n%s', iq)

mathieui's avatar
mathieui committed
813 814 815 816 817 818
        self.core.xmpp.update_roster(
            jid,
            name=name,
            groups=new_groups,
            subscription=subscription,
            callback=callback)
819

820
    @deny_anonymous
821 822
    @command_args_parser.quoted(3)
    def command_groupmove(self, args):
823 824 825
        """
        Remove the specified JID from the first specified group and add it to the second one
        """
826
        if args is None:
827
            return self.core.command.help('groupmove')
828 829 830 831 832 833
        jid = safeJID(args[0]).bare
        group_from = args[1]
        group_to = args[2]

        contact = roster[jid]
        if not contact:
834
            self.core.information('No such JID in roster', 'Error')
835 836 837 838 839 840 841
            return

        new_groups = set(contact.groups)
        if 'none' in new_groups:
            new_groups.remove('none')

        if group_to == 'none' or group_from == 'none':
842
            self.core.information('"none" is not a group.', 'Error')
843 844 845
            return

        if group_from not in new_groups:
846
            self.core.information('JID not in first group', 'Error')
847 848 849
            return

        if group_to in new_groups:
850
            self.core.information('JID already in second group', 'Error')
851 852 853
            return

        if group_to == group_from:
854
            self.core.information('The groups are the same.', 'Error')
855 856 857 858 859 860 861 862 863 864 865 866 867 868 869
            return

        roster.modified()
        new_groups.add(group_to)
        if 'none' in new_groups:
            new_groups.remove('none')

        new_groups.remove(group_from)
        name = contact.name
        subscription = contact.subscription

        def callback(iq):
            if iq:
                roster.update_contact_groups(contact)
            else:
870
                self.core.information('The group could not be set', 'Error')
871 872
                log.debug('Error in groupmove:\n%s', iq)

mathieui's avatar
mathieui committed
873 874 875 876 877 878
        self.core.xmpp.update_roster(
            jid,
            name=name,
            groups=new_groups,
            subscription=subscription,
            callback=callback)
879

880
    @deny_anonymous
881
    @command_args_parser.quoted(2)
882 883 884 885
    def command_groupremove(self, args):
        """
        Remove the specified JID from the specified group
        """
886
        if args is None:
887
            return self.core.command.help('groupremove')
888

889 890 891 892 893
        jid = safeJID(args[0]).bare
        group = args[1]

        contact = roster[jid]
        if contact is None:
894
            self.core.information('No such JID in roster', 'Error')
895 896 897 898 899 900 901 902
            return

        new_groups = set(contact.groups)
        try:
            new_groups.remove('none')
        except KeyError:
            pass
        if group not in new_groups:
903
            self.core.information('JID not in group', 'Error')
904 905 906 907 908 909 910 911 912 913 914 915 916 917 918
            return

        roster.modified()

        new_groups.remove(group)
        name = contact.name
        subscription = contact.subscription

        def callback(iq):
            if iq:
                roster.update_contact_groups(jid)
            else:
                self.core.information('The group could not be set')
                log.debug('Error in groupremove:\n%s', iq)

mathieui's avatar
mathieui committed
919 920 921 922 923 924
        self.core.xmpp.update_roster(
            jid,
            name=name,
            groups=new_groups,
            subscription=subscription,
            callback=callback)
925

926
    @deny_anonymous
927
    @command_args_parser.quoted(0, 1)
928 929 930 931 932
    def command_remove(self, args):
        """
        Remove the specified JID from the roster. i.e.: unsubscribe
        from its presence, and cancel its subscription to our.
        """
933 934
        if args:
            jid = safeJID(args[0]).bare
935 936 937 938 939
        else:
            item = self.roster_win.selected_row
            if isinstance(item, Contact):
                jid = item.bare_jid
            else:
940
                self.core.information('No roster item to remove', 'Error')
941 942 943 944
                return
        roster.remove(jid)
        del roster[jid]

945
    @deny_anonymous
946 947
    @command_args_parser.quoted(0, 1)
    def command_import(self, args):
948 949 950
        """
        Import the contacts
        """
951
        if args:
952 953 954 955 956 957 958
            if args[0].startswith('/'):
                filepath = args[0]
            else:
                filepath = path.join(getenv('HOME'), args[0])
        else:
            filepath = path.join(getenv('HOME'), 'poezio_contacts')
        if not path.isfile(filepath):
mathieui's avatar
mathieui committed
959 960
            self.core.information('The file %s does not exist' % filepath,
                                  'Error')
961 962 963 964 965 966 967 968 969 970 971 972 973
            return
        try:
            handle = open(filepath, 'r', encoding='utf-8')
            lines = handle.readlines()
            handle.close()
        except IOError:
            self.core.information('Could not open %s' % filepath, 'Error')
            log.error('Unable to correct a message', exc_info=True)
            return
        for jid in lines:
            self.command_add(jid.lstrip('\n'))
        self.core.information('Contacts imported from %s' % filepath, 'Info')

974
    @deny_anonymous
975 976
    @command_args_parser.quoted(0, 1)
    def command_export(self, args):
977 978 979
        """
        Export the contacts
        """
980
        if args:
981 982 983 984 985 986 987 988 989 990 991 992 993 994 995
            if args[0].startswith('/'):
                filepath = args[0]
            else:
                filepath = path.join(getenv('HOME'), args[0])
        else:
            filepath = path.join(getenv('HOME'), 'poezio_contacts')
        if path.isfile(filepath):
            self.core.information('The file already exists', 'Error')
            return
        elif not path.isdir(path.dirname(filepath)):
            self.core.information('Parent directory not found', 'Error')
            return
        if roster.export(filepath):
            self.core.information('Contacts exported to %s' % filepath, 'Info')
        else:
mathieui's avatar
mathieui committed
996 997
            self.core.information('Failed to export contacts to %s' % filepath,
                                  'Info')
998 999 1000 1001 1002 1003

    def completion_remove(self, the_input):
        """
        Completion for /remove
        """
        jids = [jid for jid in roster.jids()]
1004
        return Completion(the_input.auto_completion, jids, '', quotify=False)
1005 1006 1007 1008 1009 1010

    def completion_name(self, the_input):
        """Completion for /name"""
        n = the_input.get_argument_position()
        if n == 1:
            jids = [jid for jid in roster.jids()]
1011
            return Completion(the_input.new_completion, jids, n, quotify=True)
1012 1013 1014 1015 1016 1017
        return False

    def completion_groupadd(self, the_input):
        n = the_input.get_argument_position()
        if n == 1:
            jids = sorted(jid for jid in roster.jids())
mathieui's avatar
mathieui committed
1018 1019
            return Completion(
                the_input.new_completion, jids, n, '', quotify=True)
1020
        elif n == 2:
mathieui's avatar
mathieui committed
1021 1022 1023 1024
            groups = sorted(
                group for group in roster.groups if group != 'none')
            return Completion(
                the_input.new_completion, groups, n, '', quotify=True)
1025 1026 1027
        return False

    def completion_groupmove(self, the_input):
1028
        args = shell_split(the_input.text)
1029 1030 1031
        n = the_input.get_argument_position()
        if n == 1:
            jids = sorted(jid for jid in roster.jids())
mathieui's avatar
mathieui committed
1032 1033
            return Completion(
                the_input.new_completion, jids, n, '', quotify=True)
1034 1035 1036 1037 1038 1039 1040
        elif n == 2:
            contact = roster[args[1]]
            if not contact:
                return False
            groups = list(contact.groups)
            if 'none' in groups:
                groups.remove('none')
mathieui's avatar
mathieui committed
1041 1042
            return Completion(
                the_input.new_completion, groups, n, '', quotify=True)
1043 1044
        elif n == 3:
            groups = sorted(group for group in roster.groups)
mathieui's avatar
mathieui committed
1045 1046
            return Completion(
                the_input.new_completion, groups, n, '', quotify=True)
1047 1048 1049
        return False

    def completion_groupremove(self, the_input):
1050
        args = shell_split(the_input.text)
1051 1052 1053
        n = the_input.get_argument_position()
        if n == 1:
            jids = sorted(jid for jid in roster.jids())
mathieui's avatar
mathieui committed
1054 1055
            return Completion(
                the_input.new_completion, jids, n, '', quotify=True)
1056 1057 1058 1059 1060 1061 1062 1063 1064
        elif n == 2:
            contact = roster[args[1]]
            if contact is None:
                return False
            groups = sorted(contact.groups)
            try:
                groups.remove('none')
            except ValueError:
                pass
mathieui's avatar
mathieui committed
1065 1066
            return Completion(
                the_input.new_completion, groups, n, '', quotify=True)
1067 1068 1069 1070 1071 1072 1073
        return False

    def completion_deny(self, the_input):
        """
        Complete the first argument from the list of the
        contact with ask=='subscribe'
        """
mathieui's avatar
mathieui committed
1074 1075 1076
        jids = sorted(
            str(contact.bare_jid) for contact in roster.contacts.values()
            if contact.pending_in)
1077
        return Completion(the_input.new_completion, jids, 1, '', quotify=False)
1078

1079
    @deny_anonymous
1080 1081
    @command_args_parser.quoted(0, 1)
    def command_accept(self, args):
1082 1083 1084
        """
        Accept a JID from in roster. Authorize it AND subscribe to it
        """
1085
        if not args:
1086 1087 1088 1089
            item = self.roster_win.selected_row
            if isinstance(item, Contact):
                jid = item.bare_jid
            else:
1090
                self.core.information('No subscription to accept', 'Warning')
1091 1092
                return
        else:
1093
            jid = safeJID(args[0]).bare
1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105
        nodepart = safeJID(jid).user
        jid = safeJID(jid)
        # crappy transports putting resources inside the node part
        if '\\2f' in nodepart:
            jid.user = nodepart.split('\\2f')[0]
        contact = roster[jid]
        if contact is None:
            return
        contact.pending_in = False
        roster.modified()
        self.core.xmpp.send_presence(pto=jid, ptype='subscribed')
        self.core.xmpp.client_roster.send_last_presence()
mathieui's avatar
mathieui committed
1106 1107 1108 1109
        if contact.subscription in ('from',
                                    'none') and not contact.pending_out:
            self.core.xmpp.send_presence(
                pto=jid, ptype='subscribe', pnick=self.core.own_nick)
1110

1111 1112
        self.core.information('%s is now authorized' % jid, 'Roster')

1113 1114 1115
    def refresh(self):
        if self.need_resize:
            self.resize()
1116
        log.debug('  TAB   Refresh: %s', self.__class__.__name__)
1117 1118 1119 1120

        display_info = not self.size.tab_degrade_x
        display_contact_win = not self.size.tab_degrade_y

1121
        self.roster_win.refresh(roster)
1122 1123 1124 1125
        if display_info:
            self.v_separator.refresh()
            self.information_win.refresh()
            if display_contact_win:
1126 1127 1128 1129 1130 1131
                row = self.roster_win.get_selected_row()
                self.contact_info_win.refresh(row)
                if isinstance(row, Contact):
                    self.avatar_win.refresh(row.avatar)
                else:
                    self.avatar_win.refresh(None)
1132 1133 1134 1135 1136 1137 1138
        self.refresh_tab_win()
        self.input.refresh()

    def on_input(self, key, raw):
        if key == '^M':
            selected_row = self.roster_win.get_selected_row()
        res = self.input.do_command(key, raw=raw)
1139 1140
        if res:
            return not isinstance(self.input, windows.Input)
1141 1142 1143 1144 1145 1146