rostertab.py 47.6 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
9
10
import logging
log = logging.getLogger(__name__)

mathieui's avatar
mathieui committed
11
import base64
12
13
14
import curses
import difflib
import os
mathieui's avatar
mathieui committed
15
import ssl
16
from os import getenv, path
17
from functools import partial
18

19
20
21
22
23
24
25
26
27
from poezio import common
from poezio import windows
from poezio.common import safeJID
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 Completion
29

mathieui's avatar
mathieui committed
30
31
32
from poezio.tabs import Tab


33
34
35
36
37
38
class RosterInfoTab(Tab):
    """
    A tab, splitted in two, containing the roster and infos
    """
    plugin_commands = {}
    plugin_keys = {}
39
40
    def __init__(self, core):
        Tab.__init__(self, core)
41
42
43
44
45
46
47
48
49
50
51
        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()
        self.default_help_message = windows.HelpText("Enter commands with “/”. “o”: toggle offline show")
        self.input = self.default_help_message
        self.state = 'normal'
        self.key_func['^I'] = self.completion
        self.key_func["/"] = self.on_slash
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
        # 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
            self.register_command('deny', self.command_deny,
71
72
73
74
                    usage='[jid]',
                    desc='Deny your presence to the provided JID (or the '
                         'selected contact in your roster), who is asking'
                         'you to be in his/here roster.',
75
                    shortdesc='Deny a user your presence.',
76
77
                    completion=self.completion_deny)
            self.register_command('accept', self.command_accept,
78
79
80
                    usage='[jid]',
                    desc='Allow the provided JID (or the selected contact '
                         'in your roster), to see your presence.',
81
                    shortdesc='Allow a user your presence.',
82
83
                    completion=self.completion_deny)
            self.register_command('add', self.command_add,
84
85
86
87
                    usage='<jid>',
                    desc='Add the specified JID to your roster, ask him to'
                         ' allow you to see his presence, and allow him to'
                         ' see your presence.',
88
                    shortdesc='Add a user to your roster.')
89
            self.register_command('name', self.command_name,
90
91
                    usage='<jid> [name]',
                    shortdesc='Set the given JID\'s name.',
92
93
                    completion=self.completion_name)
            self.register_command('groupadd', self.command_groupadd,
94
95
96
                    usage='[<jid> <group>]|<group>',
                    desc='Add the given JID or selected line to the given group.',
                    shortdesc='Add a user to a group',
97
98
                    completion=self.completion_groupadd)
            self.register_command('groupmove', self.command_groupmove,
99
100
                    usage='<jid> <old group> <new group>',
                    desc='Move the given JID from the old group to the new group.',
101
                    shortdesc='Move a user to another group.',
102
103
                    completion=self.completion_groupmove)
            self.register_command('groupremove', self.command_groupremove,
104
105
                    usage='<jid> <group>',
                    desc='Remove the given JID from the given group.',
106
                    shortdesc='Remove a user from a group.',
107
108
                    completion=self.completion_groupremove)
            self.register_command('remove', self.command_remove,
109
110
111
112
113
                    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.',
114
                    shortdesc='Remove a user from your roster.',
115
116
                    completion=self.completion_remove)
            self.register_command('export', self.command_export,
117
118
119
120
                    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.',
121
122
                    completion=partial(self.completion_file, 1))
            self.register_command('import', self.command_import,
123
124
125
126
                    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.',
127
128
129
                    completion=partial(self.completion_file, 1))
            self.register_command('password', self.command_password,
                    usage='<password>',
130
                    shortdesc='Change your password')
131

132
        self.register_command('reconnect', self.command_reconnect,
133
134
135
                desc='Disconnect from the remote server if you are '
                     'currently connected and then connect to it again.',
                shortdesc='Disconnect and reconnect to the server.')
136
        self.register_command('disconnect', self.command_disconnect,
137
138
                desc='Disconnect from the remote server.',
                shortdesc='Disconnect from the server.')
139
        self.register_command('clear', self.command_clear,
140
                shortdesc='Clear the info buffer.')
141
        self.register_command('last_activity', self.command_last_activity,
142
143
144
                usage='<jid>',
                desc='Informs you of the last activity of a JID.',
                shortdesc='Get the activity of someone.',
145
                completion=self.core.completion.last_activity)
146
147
148
149
150
151

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

    def check_blocking(self, features):
152
        if 'urn:xmpp:blocking' in features and not self.core.xmpp.anon:
153
            self.register_command('block', self.command_block,
154
155
                    usage='[jid]',
                    shortdesc='Prevent a JID from talking to you.',
156
157
                    completion=self.completion_block)
            self.register_command('unblock', self.command_unblock,
158
159
                    usage='[jid]',
                    shortdesc='Allow a JID to talk to you.',
160
161
                    completion=self.completion_unblock)
            self.register_command('list_blocks', self.command_list_blocks,
162
                    shortdesc='Show the blocked contacts.')
163
164
165
            self.core.xmpp.del_event_handler('session_start', self.check_blocking)
            self.core.xmpp.add_event_handler('blocked_message', self.on_blocked_message)

mathieui's avatar
mathieui committed
166
    def check_saslexternal(self, features):
167
        if 'urn:xmpp:saslcert:1' in features and not self.core.xmpp.anon:
mathieui's avatar
mathieui committed
168
            self.register_command('certs', self.command_certs,
169
170
171
                                  desc='List the fingerprints of certificates'
                                       ' which can connect to your account.',
                                  shortdesc='List allowed client certs.')
mathieui's avatar
mathieui committed
172
            self.register_command('cert_add', self.command_cert_add,
173
174
175
176
177
178
                                  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.',
179
180
                                  usage='<name> <certificate path> [management]',
                                  completion=self.completion_cert_add)
mathieui's avatar
mathieui committed
181
            self.register_command('cert_disable', self.command_cert_disable,
182
183
184
185
186
                                  desc='Remove a certificate from the list '
                                       'of allowed ones. Clients currently '
                                       'using this certificate will not be '
                                       'forcefully disconnected.',
                                  shortdesc='Disable a certificate',
mathieui's avatar
mathieui committed
187
188
                                  usage='<name>')
            self.register_command('cert_revoke', self.command_cert_revoke,
189
190
191
192
193
                                  desc='Remove a certificate from the list '
                                       'of allowed ones. Clients currently '
                                       'using this certificate will be '
                                       'forcefully disconnected.',
                                  shortdesc='Revoke a certificate',
mathieui's avatar
mathieui committed
194
195
                                  usage='<name>')
            self.register_command('cert_fetch', self.command_cert_fetch,
196
197
198
                                  desc='Retrieve a certificate with its '
                                       'name. It will be stored in <path>.',
                                  shortdesc='Fetch a certificate',
199
200
                                  usage='<name> <path>',
                                  completion=self.completion_cert_fetch)
mathieui's avatar
mathieui committed
201

202
203
204
205
    @property
    def selected_row(self):
        return self.roster_win.get_selected_row()

mathieui's avatar
mathieui committed
206
207
208
209
210
211
212
    @command_args_parser.ignored
    def command_certs(self):
        """
        /certs
        """
        def cb(iq):
            if iq['type'] == 'error':
213
214
                self.core.information('Unable to retrieve the certificate list.',
                                      'Error')
mathieui's avatar
mathieui committed
215
216
217
218
219
220
221
                return
            certs = []
            for item in iq['sasl_certs']['items']:
                users = '\n'.join(item['users'])
                certs.append((item['name'], users))

            if not certs:
222
223
                return self.core.information('No certificates found', 'Info')
            msg = 'Certificates:\n'
mathieui's avatar
mathieui committed
224
225
226
227
228
229
230
231
232
233
234
            msg += '\n'.join((('  %s%s' % (item[0] + (': ' if item[1] else ''), item[1])) for item in certs))
            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:
235
            return self.core.command.help('cert_add')
mathieui's avatar
mathieui committed
236
237
        def cb(iq):
            if iq['type'] == 'error':
238
                self.core.information('Unable to add the certificate.', 'Error')
mathieui's avatar
mathieui committed
239
            else:
240
                self.core.information('Certificate added.', 'Info')
mathieui's avatar
mathieui committed
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267

        name = args[0]

        try:
            with open(args[1]) as fd:
                crt = fd.read()
            crt = crt.replace(ssl.PEM_FOOTER, '').replace(ssl.PEM_HEADER, '').replace(' ', '').replace('\n', '')
        except Exception as e:
            self.core.information('Unable to read the certificate: %s' % e, 'Error')
            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

        self.core.xmpp.plugin['xep_0257'].add_cert(name, crt, callback=cb,
                                                   allow_management=management)

268
269
270
271
272
273
274
275
276
277
278
279
280
    def completion_cert_add(self, the_input):
        """
        completion for /cert_add <name> <path> [management]
        """
        text = the_input.get_text()
        args = common.shell_split(text)
        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:
281
            return Completion(the_input.new_completion, ['true', 'false'], n)
282

mathieui's avatar
mathieui committed
283
284
285
286
287
288
    @command_args_parser.quoted(1)
    def command_cert_disable(self, args):
        """
        /cert_disable <name>
        """
        if not args:
289
            return self.core.command.help('cert_disable')
mathieui's avatar
mathieui committed
290
291
        def cb(iq):
            if iq['type'] == 'error':
292
                self.core.information('Unable to disable the certificate.', 'Error')
mathieui's avatar
mathieui committed
293
            else:
294
                self.core.information('Certificate disabled.', 'Info')
mathieui's avatar
mathieui committed
295
296
297
298
299
300
301
302
303
304
305

        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:
306
            return self.core.command.help('cert_revoke')
mathieui's avatar
mathieui committed
307
308
        def cb(iq):
            if iq['type'] == 'error':
309
                self.core.information('Unable to revoke the certificate.', 'Error')
mathieui's avatar
mathieui committed
310
            else:
311
                self.core.information('Certificate revoked.', 'Info')
mathieui's avatar
mathieui committed
312
313
314
315
316
317
318
319
320
321
322
323

        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:
324
            return self.core.command.help('cert_fetch')
mathieui's avatar
mathieui committed
325
326
        def cb(iq):
            if iq['type'] == 'error':
327
328
                self.core.information('Unable to fetch the certificate.',
                                      'Error')
mathieui's avatar
mathieui committed
329
330
331
332
333
334
335
336
337
                return

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

            if not cert:
338
                return self.core.information('Certificate not found.', 'Info')
mathieui's avatar
mathieui committed
339
340
341
342
343

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

344
            self.core.information('File stored at %s' % path, 'Info')
mathieui's avatar
mathieui committed
345
346
347
348
349
350

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

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

351
352
353
354
355
356
357
358
359
360
361
362
363
    def completion_cert_fetch(self, the_input):
        """
        completion for /cert_fetch <name> <path>
        """
        text = the_input.get_text()
        args = common.shell_split(text)
        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)

364
365
366
367
368
369
370
371
372
373
374
375
376
    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:
            log.debug('Received message from nonexistent tab: %s', message['from'])
        message = '\x19%(info_col)s}Cannot send message to %(jid)s: contact blocked' % {
                'info_col': dump_tuple(get_theme().COLOR_INFORMATION_TEXT),
                'jid': message['from'],
            }
        tab.add_message(message)

377
378
    @command_args_parser.quoted(0, 1)
    def command_block(self, args):
379
380
381
382
383
384
385
386
387
388
        """
        /block [jid]
        """
        def callback(iq):
            if iq['type'] == 'error':
                return self.core.information('Could not block the contact.', 'Error')
            elif iq['type'] == 'result':
                return self.core.information('Contact blocked.', 'Info')

        item = self.roster_win.selected_row
389
390
        if args:
            jid = safeJID(args[0])
391
392
393
394
        elif isinstance(item, Contact):
            jid = item.bare_jid
        elif isinstance(item, Resource):
            jid = item.jid.bare
395
        self.core.xmpp.plugin['xep_0191'].block(jid, callback=callback)
396
397
398
399
400
401
402

    def completion_block(self, the_input):
        """
        Completion for /block
        """
        if the_input.get_argument_position() == 1:
            jids = roster.jids()
403
            return Completion(the_input.new_completion, jids, 1, '', quotify=False)
404

405
406
    @command_args_parser.quoted(0, 1)
    def command_unblock(self, args):
407
408
409
410
411
412
413
414
415
416
        """
        /unblock [jid]
        """
        def callback(iq):
            if iq['type'] == 'error':
                return self.core.information('Could not unblock the contact.', 'Error')
            elif iq['type'] == 'result':
                return self.core.information('Contact unblocked.', 'Info')

        item = self.roster_win.selected_row
417
418
        if args:
            jid = safeJID(args[0])
419
420
421
422
        elif isinstance(item, Contact):
            jid = item.bare_jid
        elif isinstance(item, Resource):
            jid = item.jid.bare
423
        self.core.xmpp.plugin['xep_0191'].unblock(jid, callback=callback)
424
425
426
427
428

    def completion_unblock(self, the_input):
        """
        Completion for /unblock
        """
429
430
431
432
        def on_result(iq):
            if iq['type'] == 'error':
                return
            l = sorted(str(item) for item in iq['blocklist']['items'])
433
            return Completion(the_input.new_completion, l, 1, quotify=False)
434

435
        if the_input.get_argument_position():
436
437
            self.core.xmpp.plugin['xep_0191'].get_blocked(callback=on_result)
        return True
438

439
440
    @command_args_parser.ignored
    def command_list_blocks(self):
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
        """
        /list_blocks
        """
        def callback(iq):
            if iq['type'] == 'error':
                return self.core.information('Could not retrieve the blocklist.', 'Error')
            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')

456
        self.core.xmpp.plugin['xep_0191'].get_blocked(callback=callback)
457

458
459
    @command_args_parser.ignored
    def command_reconnect(self):
460
461
462
        """
        /reconnect
        """
463
464
465
466
        if self.core.xmpp.is_connected():
            self.core.disconnect(reconnect=True)
        else:
            self.core.xmpp.connect()
467

468
469
    @command_args_parser.ignored
    def command_disconnect(self):
470
471
472
473
474
        """
        /disconnect
        """
        self.core.disconnect()

475
476
    @command_args_parser.quoted(0, 1)
    def command_last_activity(self, args):
477
478
479
480
        """
        /activity [jid]
        """
        item = self.roster_win.selected_row
481
482
        if args:
            jid = args[0]
483
484
485
        elif isinstance(item, Contact):
            jid = item.bare_jid
        elif isinstance(item, Resource):
486
            jid = item.jid
487
488
489
        else:
            self.core.information('No JID selected.', 'Error')
            return
490
        self.core.command.last_activity(jid)
491
492

    def resize(self):
493
        self.need_resize = False
494
495
496
497
498
499
500
501
502
503
504
        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
505
                contact_win_h = 4
506
507
508
509
510
511
512
513
514
515
516
517
518
519
        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:
            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)
            if display_contact_win:
520
                self.contact_info_win.resize(contact_win_h,
521
522
                                             info_width,
                                             self.height - tab_win_height
523
                                             - contact_win_h - 1,
524
525
526
                                             roster_width + 1)
        self.roster_win.resize(self.height - 1 - Tab.tab_win_height(),
                               roster_width, 0, 0)
527
        self.input.resize(1, self.width, self.height-1, 0)
528
        self.default_help_message.resize(1, self.width, self.height-1, 0)
529
530
531
532
533
534
535

    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)

536
    def completion_file(self, complete_number, the_input):
537
        """
538
539
540
        Generic quoted completion for files/paths
        (use functools.partial to use directly as a completion
        for a command)
541
542
        """
        text = the_input.get_text()
543
544
545
546
547
        args = common.shell_split(text)
        n = the_input.get_argument_position()
        if n == complete_number:
            if args[n-1] == '' or len(args) < n+1:
                home = os.getenv('HOME') or '/'
548
                return Completion(the_input.new_completion, [home, '/tmp'], n, quotify=True)
549
550
551
552
553
554
555
            path_ = args[n]
            if path.isdir(path_):
                dir_ = path_
                base = ''
            else:
                dir_ = path.dirname(path_)
                base = path.basename(path_)
556
            try:
557
558
                names = os.listdir(dir_)
            except OSError:
559
                names = []
560
561
562
563
564
            names_filtered = [name for name in names if name.startswith(base)]
            if names_filtered:
                names = names_filtered
            if not names:
                names = [path_]
565
566
            end_list = []
            for name in names:
567
                value = os.path.join(dir_, name)
568
569
570
                if not name.startswith('.'):
                    end_list.append(value)

571
            return Completion(the_input.new_completion, end_list, n, quotify=True)
572

573
574
    @command_args_parser.ignored
    def command_clear(self):
575
576
577
578
579
580
581
582
        """
        /clear
        """
        self.core.information_buffer.messages = []
        self.information_win.rebuild_everything(self.core.information_buffer)
        self.core.information_win.rebuild_everything(self.core.information_buffer)
        self.refresh()

583
584
    @command_args_parser.quoted(1)
    def command_password(self, args):
585
586
587
588
589
590
        """
        /password <password>
        """
        def callback(iq):
            if iq['type'] == 'result':
                self.core.information('Password updated', 'Account')
591
                if config.get('password'):
592
                    config.silent_set('password', args[0])
593
594
            else:
                self.core.information('Unable to change the password', 'Account')
595
        self.core.xmpp.plugin['xep_0077'].change_password(args[0], callback=callback)
596

597
598
    @command_args_parser.quoted(0, 1)
    def command_deny(self, args):
599
600
601
602
        """
        /deny [jid]
        Denies a JID from our roster
        """
603
        if not args:
604
605
606
607
608
609
610
            item = self.roster_win.selected_row
            if isinstance(item, Contact):
                jid = item.bare_jid
            else:
                self.core.information('No subscription to deny')
                return
        else:
611
            jid = safeJID(args[0]).bare
612
613
614
615
616
617
618
            if not jid in [jid for jid in roster.jids()]:
                self.core.information('No subscription to deny')
                return

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

622
    @command_args_parser.quoted(1)
623
624
625
626
627
    def command_add(self, args):
        """
        Add the specified JID to the roster, and set automatically
        accept the reverse subscription
        """
628
        if args is None:
629
            self.core.information('No JID specified', 'Error')
630
631
632
633
            return
        jid = safeJID(safeJID(args[0]).bare)
        if not str(jid):
            self.core.information('The provided JID (%s) is not valid' % (args[0],), 'Error')
634
635
636
637
638
            return
        if jid in roster and roster[jid].subscription in ('to', 'both'):
            return self.core.information('Already subscribed.', 'Roster')
        roster.add(jid)
        roster.modified()
639
        self.core.information('%s was added to the roster' % jid, 'Roster')
640

louiz’'s avatar
louiz’ committed
641
    @command_args_parser.quoted(1, 1)
642
    def command_name(self, args):
643
644
645
646
647
648
649
        """
        Set a name for the specified JID in your roster
        """
        def callback(iq):
            if not iq:
                self.core.information('The name could not be set.', 'Error')
                log.debug('Error in /name:\n%s', iq)
louiz’'s avatar
louiz’ committed
650
        if args is None:
651
            return self.core.command.help('name')
652
653
654
655
656
        jid = safeJID(args[0]).bare
        name = args[1] if len(args) == 2 else ''

        contact = roster[jid]
        if contact is None:
657
            self.core.information('No such JID in roster', 'Error')
658
659
660
661
662
663
            return

        groups = set(contact.groups)
        if 'none' in groups:
            groups.remove('none')
        subscription = contact.subscription
664
665
        self.core.xmpp.update_roster(jid, name=name, groups=groups,
                                     subscription=subscription, callback=callback)
666

667
    @command_args_parser.quoted(1, 1)
668
669
670
671
    def command_groupadd(self, args):
        """
        Add the specified JID to the specified group
        """
672
        if args is None:
673
            return self.core.command.help('groupadd')
674
675
676
677
678
679
680
681
682
683
684
685
        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]
686
687
688

        contact = roster[jid]
        if contact is None:
689
            self.core.information('No such JID in roster', 'Error')
690
691
692
693
            return

        new_groups = set(contact.groups)
        if group in new_groups:
694
            self.core.information('JID already in group', 'Error')
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
            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)

714
715
        self.core.xmpp.update_roster(jid, name=name, groups=new_groups,
                                     subscription=subscription, callback=callback)
716

717
718
    @command_args_parser.quoted(3)
    def command_groupmove(self, args):
719
720
721
        """
        Remove the specified JID from the first specified group and add it to the second one
        """
722
        if args is None:
723
            return self.core.command.help('groupmove')
724
725
726
727
728
729
        jid = safeJID(args[0]).bare
        group_from = args[1]
        group_to = args[2]

        contact = roster[jid]
        if not contact:
730
            self.core.information('No such JID in roster', 'Error')
731
732
733
734
735
736
737
            return

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

        if group_to == 'none' or group_from == 'none':
738
            self.core.information('"none" is not a group.', 'Error')
739
740
741
            return

        if group_from not in new_groups:
742
            self.core.information('JID not in first group', 'Error')
743
744
745
            return

        if group_to in new_groups:
746
            self.core.information('JID already in second group', 'Error')
747
748
749
            return

        if group_to == group_from:
750
            self.core.information('The groups are the same.', 'Error')
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
            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:
                self.core.information('The group could not be set')
                log.debug('Error in groupmove:\n%s', iq)

769
770
        self.core.xmpp.update_roster(jid, name=name, groups=new_groups,
                                     subscription=subscription, callback=callback)
771

772
    @command_args_parser.quoted(2)
773
774
775
776
    def command_groupremove(self, args):
        """
        Remove the specified JID from the specified group
        """
777
        if args is None:
778
            return self.core.command.help('groupremove')
779

780
781
782
783
784
        jid = safeJID(args[0]).bare
        group = args[1]

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

        new_groups = set(contact.groups)
        try:
            new_groups.remove('none')
        except KeyError:
            pass
        if group not in new_groups:
794
            self.core.information('JID not in group', 'Error')
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
            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)

810
811
        self.core.xmpp.update_roster(jid, name=name, groups=new_groups,
                                     subscription=subscription, callback=callback)
812

813
    @command_args_parser.quoted(0, 1)
814
815
816
817
818
    def command_remove(self, args):
        """
        Remove the specified JID from the roster. i.e.: unsubscribe
        from its presence, and cancel its subscription to our.
        """
819
820
        if args:
            jid = safeJID(args[0]).bare
821
822
823
824
825
826
827
828
829
830
        else:
            item = self.roster_win.selected_row
            if isinstance(item, Contact):
                jid = item.bare_jid
            else:
                self.core.information('No roster item to remove')
                return
        roster.remove(jid)
        del roster[jid]

831
832
    @command_args_parser.quoted(0, 1)
    def command_import(self, args):
833
834
835
        """
        Import the contacts
        """
836
        if args:
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
            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):
            self.core.information('The file %s does not exist' % filepath, 'Error')
            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')

858
859
    @command_args_parser.quoted(0, 1)
    def command_export(self, args):
860
861
862
        """
        Export the contacts
        """
863
        if args:
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
            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:
            self.core.information('Failed to export contacts to %s' % filepath, 'Info')

    def completion_remove(self, the_input):
        """
        Completion for /remove
        """
        jids = [jid for jid in roster.jids()]
886
        return Completion(the_input.auto_completion, jids, '', quotify=False)
887
888
889
890
891
892

    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()]
893
            return Completion(the_input.new_completion, jids, n, quotify=True)
894
895
896
897
898
899
        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())
900
            return Completion(the_input.new_completion, jids, n, '', quotify=True)
901
902
        elif n == 2:
            groups = sorted(group for group in roster.groups if group != 'none')
903
            return Completion(the_input.new_completion, groups, n, '', quotify=True)
904
905
906
907
908
909
910
        return False

    def completion_groupmove(self, the_input):
        args = common.shell_split(the_input.text)
        n = the_input.get_argument_position()
        if n == 1:
            jids = sorted(jid for jid in roster.jids())
911
            return Completion(the_input.new_completion, jids, n, '', quotify=True)
912
913
914
915
916
917
918
        elif n == 2:
            contact = roster[args[1]]
            if not contact:
                return False
            groups = list(contact.groups)
            if 'none' in groups:
                groups.remove('none')
919
            return Completion(the_input.new_completion, groups, n, '', quotify=True)
920
921
        elif n == 3:
            groups = sorted(group for group in roster.groups)
922
            return Completion(the_input.new_completion, groups, n, '', quotify=True)
923
924
925
926
927
928
929
        return False

    def completion_groupremove(self, the_input):
        args = common.shell_split(the_input.text)
        n = the_input.get_argument_position()
        if n == 1:
            jids = sorted(jid for jid in roster.jids())
930
            return Completion(the_input.new_completion, jids, n, '', quotify=True)
931
932
933
934
935
936
937
938
939
        elif n == 2:
            contact = roster[args[1]]
            if contact is None:
                return False
            groups = sorted(contact.groups)
            try:
                groups.remove('none')
            except ValueError:
                pass
940
            return Completion(the_input.new_completion, groups, n, '', quotify=True)
941
942
943
944
945
946
947
948
949
        return False

    def completion_deny(self, the_input):
        """
        Complete the first argument from the list of the
        contact with ask=='subscribe'
        """
        jids = sorted(str(contact.bare_jid) for contact in roster.contacts.values()
             if contact.pending_in)
950
        return Completion(the_input.new_completion, jids, 1, '', quotify=False)
951

952
953
    @command_args_parser.quoted(0, 1)
    def command_accept(self, args):
954
955
956
        """
        Accept a JID from in roster. Authorize it AND subscribe to it
        """
957
        if not args:
958
959
960
961
962
963
964
            item = self.roster_win.selected_row
            if isinstance(item, Contact):
                jid = item.bare_jid
            else:
                self.core.information('No subscription to accept')
                return
        else:
965
            jid = safeJID(args[0]).bare
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
        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()
        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)

981
982
        self.core.information('%s is now authorized' % jid, 'Roster')

983
984
985
    def refresh(self):
        if self.need_resize:
            self.resize()
986
        log.debug('  TAB   Refresh: %s', self.__class__.__name__)
987
988
989
990

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

991
        self.roster_win.refresh(roster)
992
993
994
995
996
997
        if display_info:
            self.v_separator.refresh()
            self.information_win.refresh()
            if display_contact_win:
                self.contact_info_win.refresh(
                        self.roster_win.get_selected_row())
998
999
1000
        self.refresh_tab_win()
        self.input.refresh()

For faster browsing, not all history is shown. View entire blame