commands.py 44.9 KB
Newer Older
mathieui's avatar
mathieui committed
1 2 3 4
"""
Global commands which are to be linked to the Core class
"""

5
import asyncio
6
from xml.etree import ElementTree as ET
7
from typing import List, Optional, Tuple
8
import logging
mathieui's avatar
mathieui committed
9

10
from slixmpp import Iq, JID, InvalidJID
11 12
from slixmpp.exceptions import XMPPError
from slixmpp.xmlstream.xmlstream import NotConnectedError
louiz’'s avatar
louiz’ committed
13 14 15
from slixmpp.xmlstream.stanzabase import StanzaBase
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
mathieui's avatar
mathieui committed
16

17 18 19
from poezio import common
from poezio import pep
from poezio import tabs
20
from poezio import multiuserchat as muc
21 22 23
from poezio.bookmarks import Bookmark
from poezio.common import safeJID
from poezio.config import config, DEFAULT_CONFIG, options as config_opts
24
from poezio.contact import Contact, Resource
25
from poezio.decorators import deny_anonymous
26 27 28 29
from poezio.plugin import PluginConfig
from poezio.roster import roster
from poezio.theming import dump_tuple, get_theme
from poezio.decorators import command_args_parser
mathieui's avatar
mathieui committed
30
from poezio.core.structs import Command, POSSIBLE_SHOW
mathieui's avatar
mathieui committed
31 32


33 34 35
log = logging.getLogger(__name__)


36 37 38 39 40
class CommandCore:
    def __init__(self, core):
        self.core = core

    @command_args_parser.quoted(0, 1)
41
    def help(self, args):
42 43 44 45 46 47 48
        """
        /help [command_name]
        """
        if not args:
            color = dump_tuple(get_theme().COLOR_HELP_COMMANDS)
            acc = []
            buff = ['Global commands:']
mathieui's avatar
mathieui committed
49
            for name, command in self.core.commands.items():
50
                if isinstance(command, Command):
mathieui's avatar
mathieui committed
51 52
                    acc.append('  \x19%s}%s\x19o - %s' % (color, name,
                                                          command.short_desc))
53
                else:
54
                    acc.append('  \x19%s}%s\x19o' % (color, name))
55 56 57 58
            acc = sorted(acc)
            buff.extend(acc)
            acc = []
            buff.append('Tab-specific commands:')
59
            tab_commands = self.core.tabs.current_tab.commands
mathieui's avatar
mathieui committed
60
            for name, command in tab_commands.items():
61
                if isinstance(command, Command):
mathieui's avatar
mathieui committed
62 63
                    acc.append('  \x19%s}%s\x19o - %s' % (color, name,
                                                          command.short_desc))
64
                else:
65
                    acc.append('  \x19%s}%s\x19o' % (color, name))
66 67 68 69 70 71 72 73
            acc = sorted(acc)
            buff.extend(acc)

            msg = '\n'.join(buff)
            msg += "\nType /help <command_name> to know what each command does"
        else:
            command = args[0].lstrip('/').strip()

74
            tab_commands = self.core.tabs.current_tab.commands
75 76 77 78
            if command in tab_commands:
                tup = tab_commands[command]
            elif command in self.core.commands:
                tup = self.core.commands[command]
mathieui's avatar
mathieui committed
79
            else:
80
                self.core.information('Unknown command: %s' % command, 'Error')
81 82 83 84
                return
            if isinstance(tup, Command):
                msg = 'Usage: /%s %s\n' % (command, tup.usage)
                msg += tup.desc
mathieui's avatar
mathieui committed
85
            else:
86
                msg = tup[1]
87
        self.core.information(msg, 'Help')
88 89

    @command_args_parser.quoted(1)
90
    def runkey(self, args):
91 92 93
        """
        /runkey <key>
        """
mathieui's avatar
mathieui committed
94

95 96 97 98 99
        def replace_line_breaks(key):
            "replace ^J with \n"
            if key == '^J':
                return '\n'
            return key
mathieui's avatar
mathieui committed
100

101
        if args is None:
102
            return self.help('runkey')
103
        char = args[0]
104
        func = self.core.key_func.get(char, None)
105 106
        if func:
            func()
mathieui's avatar
mathieui committed
107
        else:
108
            res = self.core.do_command(replace_line_breaks(char), False)
109
            if res:
110
                self.core.refresh_window()
111 112

    @command_args_parser.quoted(1, 1, [None])
113
    def status(self, args):
114 115 116 117
        """
        /status <status> [msg]
        """
        if args is None:
118
            return self.help('status')
119

120
        if args[0] not in POSSIBLE_SHOW.keys():
121
            return self.help('status')
122 123 124 125

        show = POSSIBLE_SHOW[args[0]]
        msg = args[1]

126
        pres = self.core.xmpp.make_presence()
127 128 129
        if msg:
            pres['status'] = msg
        pres['type'] = show
130
        self.core.events.trigger('send_normal_presence', pres)
mathieui's avatar
mathieui committed
131
        pres.send()
132
        current = self.core.tabs.current_tab
133 134 135
        is_muctab = isinstance(current, tabs.MucTab)
        if is_muctab and current.joined and show in ('away', 'xa'):
            current.send_chat_state('inactive')
136
        for tab in self.core.tabs:
137
            if isinstance(tab, tabs.MucTab) and tab.joined:
138
                muc.change_show(self.core.xmpp, tab.jid, tab.own_nick, show,
mathieui's avatar
mathieui committed
139
                                msg)
140 141
            if hasattr(tab, 'directed_presence'):
                del tab.directed_presence
142
        self.core.set_status(show, msg)
143 144 145 146
        if is_muctab and current.joined and show not in ('away', 'xa'):
            current.send_chat_state('active')

    @command_args_parser.quoted(1, 2, [None, None])
147
    def presence(self, args):
148 149 150 151
        """
        /presence <JID> [type] [status]
        """
        if args is None:
152
            return self.help('presence')
153

154
        jid, ptype, status = args[0], args[1], args[2]
155
        if jid == '.' and isinstance(self.core.tabs.current_tab, tabs.ChatTab):
156
            jid = self.core.tabs.current_tab.jid
157 158
        if ptype == 'available':
            ptype = None
159
        try:
mathieui's avatar
mathieui committed
160 161
            pres = self.core.xmpp.make_presence(
                pto=jid, ptype=ptype, pstatus=status)
162
            self.core.events.trigger('send_normal_presence', pres)
163
            pres.send()
164
        except (XMPPError, NotConnectedError):
165
            self.core.information('Could not send directed presence', 'Error')
mathieui's avatar
mathieui committed
166 167
            log.debug(
                'Could not send directed presence to %s', jid, exc_info=True)
mathieui's avatar
mathieui committed
168
            return
mathieui's avatar
mathieui committed
169
        tab = self.core.tabs.by_name(jid)
170
        if tab:
171
            if ptype in ('xa', 'away'):
172 173 174 175 176
                tab.directed_presence = False
                chatstate = 'inactive'
            else:
                tab.directed_presence = True
                chatstate = 'active'
177
            if tab == self.core.tabs.current_tab:
178 179 180 181
                tab.send_chat_state(chatstate, True)
            if isinstance(tab, tabs.MucTab):
                for private in tab.privates:
                    private.directed_presence = tab.directed_presence
182 183
                if self.core.tabs.current_tab in tab.privates:
                    self.core.tabs.current_tab.send_chat_state(chatstate, True)
184 185

    @command_args_parser.quoted(1)
186
    def theme(self, args=None):
187 188
        """/theme <theme name>"""
        if args is None:
189
            return self.help('theme')
mathieui's avatar
mathieui committed
190
        self.set('theme %s' % (args[0], ))
191 192

    @command_args_parser.quoted(1)
193
    def win(self, args):
194
        """
mathieui's avatar
mathieui committed
195
        /win <number or name>
196 197
        """
        if args is None:
198
            return self.help('win')
199

mathieui's avatar
mathieui committed
200
        name = args[0]
mathieui's avatar
mathieui committed
201
        try:
mathieui's avatar
mathieui committed
202
            number = int(name)
mathieui's avatar
mathieui committed
203
        except ValueError:
mathieui's avatar
mathieui committed
204
            number = -1
205
            name = name.lower()
206
        if number != -1 and self.core.tabs.current_tab == number:
mathieui's avatar
mathieui committed
207
            return
mathieui's avatar
mathieui committed
208
        prev_nb = self.core.previous_tab_nb
209 210
        self.core.previous_tab_nb = self.core.tabs.current_tab
        old_tab = self.core.tabs.current_tab
mathieui's avatar
mathieui committed
211 212 213 214
        if 0 <= number < len(self.core.tabs):
            if not self.core.tabs[number]:
                self.core.previous_tab_nb = prev_nb
                return
215
            self.core.tabs.set_current_index(number)
mathieui's avatar
mathieui committed
216
        else:
217
            match = self.core.tabs.find_match(name)
218
            if match is None:
219
                return
220
            self.core.tabs.set_current_tab(match)
221 222

    @command_args_parser.quoted(2)
223
    def move_tab(self, args):
224 225 226 227
        """
        /move_tab old_pos new_pos
        """
        if args is None:
228
            return self.help('move_tab')
229

230
        current_tab = self.core.tabs.current_tab
231 232 233 234 235 236 237 238 239 240 241 242
        if args[0] == '.':
            args[0] = current_tab.nb
        if args[1] == '.':
            args[1] = current_tab.nb

        def get_nb_from_value(value):
            "parse the cmdline to guess the tab the users wants"
            ref = None
            try:
                ref = int(value)
            except ValueError:
                old_tab = None
243
                for tab in self.core.tabs:
244 245 246
                    if not old_tab and value == tab.name:
                        old_tab = tab
                if not old_tab:
mathieui's avatar
mathieui committed
247 248
                    self.core.information("Tab %s does not exist" % args[0],
                                          "Error")
249 250 251
                    return None
                ref = old_tab.nb
            return ref
mathieui's avatar
mathieui committed
252

253 254 255
        old = get_nb_from_value(args[0])
        new = get_nb_from_value(args[1])
        if new is None or old is None:
256 257
            return self.core.information('Unable to move the tab.', 'Info')
        result = self.core.insert_tab(old, new)
258
        if not result:
259 260
            self.core.information('Unable to move the tab.', 'Info')
        self.core.refresh_window()
261 262

    @command_args_parser.quoted(0, 1)
263
    def list(self, args: List[str]) -> None:
264 265 266 267 268
        """
        /list [server]
        Opens a MucListTab containing the list of the room in the specified server
        """
        if args is None:
269
            return self.help('list')
270
        elif args:
271 272 273 274
            try:
                jid = JID(args[0])
            except InvalidJID:
                return self.core.information('Invalid server %r' % jid, 'Error')
275
        else:
276
            if not isinstance(self.core.tabs.current_tab, tabs.MucTab):
mathieui's avatar
mathieui committed
277 278
                return self.core.information('Please provide a server',
                                             'Error')
279 280 281
            jid = self.core.tabs.current_tab.jid
        if jid is None or not jid.domain:
            return None
282
        jid = JID(jid.domain)
283
        list_tab = tabs.MucListTab(self.core, jid)
284
        self.core.add_tab(list_tab, True)
285
        cb = list_tab.on_muc_list_item_received
mathieui's avatar
mathieui committed
286
        self.core.xmpp.plugin['xep_0030'].get_items(jid=jid, callback=cb)
287 288

    @command_args_parser.quoted(1)
289
    def version(self, args):
290 291 292 293
        """
        /version <jid>
        """
        if args is None:
294
            return self.help('version')
295
        jid = safeJID(args[0])
296
        if jid.resource or jid not in roster or not roster[jid].resources:
297 298
            self.core.xmpp.plugin['xep_0092'].get_version(
                jid, callback=self.core.handler.on_version_result)
299 300
        elif jid in roster:
            for resource in roster[jid].resources:
301 302
                self.core.xmpp.plugin['xep_0092'].get_version(
                    resource.jid, callback=self.core.handler.on_version_result)
303

mathieui's avatar
mathieui committed
304
    def _empty_join(self):
305
        tab = self.core.tabs.current_tab
mathieui's avatar
mathieui committed
306 307
        if not isinstance(tab, (tabs.MucTab, tabs.PrivateTab)):
            return (None, None)
308
        room = tab.jid.bare
mathieui's avatar
mathieui committed
309 310 311
        nick = tab.own_nick
        return (room, nick)

312
    def _parse_join_jid(self, jid_string: str) -> Tuple[Optional[str], Optional[str]]:
mathieui's avatar
mathieui committed
313
        # we try to join a server directly
314 315 316 317 318 319 320 321 322
        try:
            if jid_string.startswith('@'):
                server_root = True
                info = JID(jid_string[1:])
            else:
                info = JID(jid_string)
                server_root = False
        except InvalidJID:
            return (None, None)
mathieui's avatar
mathieui committed
323

324
        set_nick = ''  # type: Optional[str]
mathieui's avatar
mathieui committed
325 326 327 328 329 330 331
        if len(jid_string) > 1 and jid_string.startswith('/'):
            set_nick = jid_string[1:]
        elif info.resource:
            set_nick = info.resource

        # happens with /join /nickname, which is OK
        if info.bare == '':
332
            tab = self.core.tabs.current_tab
mathieui's avatar
mathieui committed
333 334 335
            if not isinstance(tab, tabs.MucTab):
                room, set_nick = (None, None)
            else:
336
                room = tab.jid.bare
mathieui's avatar
mathieui committed
337 338 339 340 341 342 343 344
                if not set_nick:
                    set_nick = tab.own_nick
        else:
            room = info.bare
            # no server is provided, like "/join hello":
            # use the server of the current room if available
            # check if the current room's name has a server
            if room.find('@') == -1 and not server_root:
345
                tab = self.core.tabs.current_tab
346 347
                if isinstance(tab, tabs.MucTab) and tab.jid.domain:
                    room += '@%s' % tab.jid.domain
mathieui's avatar
mathieui committed
348 349
        return (room, set_nick)

350
    @command_args_parser.quoted(0, 2)
351
    def join(self, args):
352 353 354 355
        """
        /join [room][/nick] [password]
        """
        if len(args) == 0:
mathieui's avatar
mathieui committed
356
            room, nick = self._empty_join()
mathieui's avatar
mathieui committed
357
        else:
mathieui's avatar
mathieui committed
358 359
            room, nick = self._parse_join_jid(args[0])
        if not room and not nick:
mathieui's avatar
mathieui committed
360
            return  # nothing was parsed
mathieui's avatar
mathieui committed
361

362
        room = room.lower()
mathieui's avatar
mathieui committed
363 364 365 366 367 368 369 370 371
        if nick == '':
            nick = self.core.own_nick

        # a password is provided
        if len(args) == 2:
            password = args[1]
        else:
            password = config.get_by_tabname('password', room, fallback=False)

372 373
        if room in self.core.pending_invites:
            del self.core.pending_invites[room]
mathieui's avatar
mathieui committed
374

375
        tab = self.core.tabs.by_name_and_class(room, tabs.MucTab)
mathieui's avatar
mathieui committed
376 377 378
        # New tab
        if tab is None:
            tab = self.core.open_new_room(room, nick, password=password)
379
            tab.join()
mathieui's avatar
mathieui committed
380
        else:
381
            self.core.focus_tab(tab)
382
            if tab.own_nick == nick and tab.joined:
383
                self.core.information('/join: Nothing to do.', 'Info')
384 385 386 387
            else:
                tab.command_part('')
                tab.own_nick = nick
                tab.password = password
mathieui's avatar
mathieui committed
388
                tab.join()
mathieui's avatar
mathieui committed
389

390
        if config.get('bookmark_on_join'):
mathieui's avatar
mathieui committed
391 392
            method = 'remote' if config.get(
                'use_remote_bookmarks') else 'local'
393 394
            self._add_bookmark('%s/%s' % (room, nick), True, password, method)

395
        if tab == self.core.tabs.current_tab:
mathieui's avatar
mathieui committed
396 397
            tab.refresh()
            self.core.doupdate()
398 399

    @command_args_parser.quoted(0, 2)
400
    def bookmark_local(self, args):
401 402 403
        """
        /bookmark_local [room][/nick] [password]
        """
404 405
        if not args and not isinstance(self.core.tabs.current_tab,
                                       tabs.MucTab):
406 407 408
            return
        password = args[1] if len(args) > 1 else None
        jid = args[0] if args else None
mathieui's avatar
mathieui committed
409

410
        self._add_bookmark(jid, True, password, 'local')
411 412

    @command_args_parser.quoted(0, 3)
413
    def bookmark(self, args):
414 415 416
        """
        /bookmark [room][/nick] [autojoin] [password]
        """
417 418
        if not args and not isinstance(self.core.tabs.current_tab,
                                       tabs.MucTab):
419 420 421 422 423
            return
        jid = args[0] if args else ''
        password = args[2] if len(args) > 2 else None

        if not config.get('use_remote_bookmarks'):
424
            return self._add_bookmark(jid, True, password, 'local')
425 426 427

        if len(args) > 1:
            autojoin = False if args[1].lower() != 'true' else True
428
        else:
429 430
            autojoin = True

431
        self._add_bookmark(jid, autojoin, password, 'remote')
432 433 434 435

    def _add_bookmark(self, jid, autojoin, password, method):
        nick = None
        if not jid:
436
            tab = self.core.tabs.current_tab
437
            roomname = tab.jid.bare
438
            if tab.joined and tab.own_nick != self.core.own_nick:
439 440 441 442
                nick = tab.own_nick
            if password is None and tab.password is not None:
                password = tab.password
        elif jid == '*':
443
            return self._add_wildcard_bookmarks(method)
444
        else:
445 446 447
            info = safeJID(jid)
            roomname, nick = info.bare, info.resource
            if roomname == '':
448
                tab = self.core.tabs.current_tab
449
                if not isinstance(tab, tabs.MucTab):
450
                    return
451
                roomname = tab.jid.bare
452
        bookmark = self.core.bookmarks[roomname]
453 454
        if bookmark is None:
            bookmark = Bookmark(roomname)
455
            self.core.bookmarks.append(bookmark)
456 457 458 459 460 461
        bookmark.method = method
        bookmark.autojoin = autojoin
        if nick:
            bookmark.nick = nick
        if password:
            bookmark.password = password
mathieui's avatar
mathieui committed
462

463
        self.core.bookmarks.save_local()
464 465
        self.core.bookmarks.save_remote(self.core.xmpp,
                                        self.core.handler.on_bookmark_result)
466 467 468

    def _add_wildcard_bookmarks(self, method):
        new_bookmarks = []
469
        for tab in self.core.get_tabs(tabs.MucTab):
470
            bookmark = self.core.bookmarks[tab.jid.bare]
471
            if not bookmark:
472
                bookmark = Bookmark(tab.jid.bare, autojoin=True, method=method)
473 474 475 476
                new_bookmarks.append(bookmark)
            else:
                bookmark.method = method
                new_bookmarks.append(bookmark)
477 478 479 480
                self.core.bookmarks.remove(bookmark)
        new_bookmarks.extend(self.core.bookmarks.bookmarks)
        self.core.bookmarks.set(new_bookmarks)
        self.core.bookmarks.save_local()
481 482
        self.core.bookmarks.save_remote(self.core.xmpp,
                                        self.core.handler.on_bookmark_result)
483 484

    @command_args_parser.ignored
485
    def bookmarks(self):
486
        """/bookmarks"""
487 488
        tab = self.core.tabs.by_name_and_class('Bookmarks', tabs.BookmarksTab)
        old_tab = self.core.tabs.current_tab
489
        if tab:
490
            self.core.tabs.set_current_tab(tab)
mathieui's avatar
mathieui committed
491
        else:
492
            tab = tabs.BookmarksTab(self.core, self.core.bookmarks)
493
            self.core.tabs.append(tab)
494
            self.core.tabs.set_current_tab(tab)
495 496

    @command_args_parser.quoted(0, 1)
497
    def remove_bookmark(self, args):
498 499 500 501
        """/remove_bookmark [jid]"""

        def cb(success):
            if success:
502
                self.core.information('Bookmark deleted', 'Info')
503
            else:
mathieui's avatar
mathieui committed
504 505
                self.core.information('Error while deleting the bookmark',
                                      'Error')
mathieui's avatar
mathieui committed
506

507
        if not args:
508
            tab = self.core.tabs.current_tab
509 510
            if isinstance(tab, tabs.MucTab) and self.core.bookmarks[tab.jid.bare]:
                self.core.bookmarks.remove(tab.jid.bare)
511
                self.core.bookmarks.save(self.core.xmpp, callback=cb)
512
            else:
513
                self.core.information('No bookmark to remove', 'Info')
mathieui's avatar
mathieui committed
514
        else:
515 516 517
            if self.core.bookmarks[args[0]]:
                self.core.bookmarks.remove(args[0])
                self.core.bookmarks.save(self.core.xmpp, callback=cb)
518
            else:
519
                self.core.information('No bookmark to remove', 'Info')
520

521
    @deny_anonymous
522
    @command_args_parser.quoted(0, 1)
523
    def accept(self, args):
524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557
        """
        Accept a JID. Authorize it AND subscribe to it
        """
        if not args:
            tab = self.core.tabs.current_tab
            RosterInfoTab = tabs.RosterInfoTab
            if not isinstance(tab, RosterInfoTab):
                return self.core.information('No JID specified', 'Error')
            else:
                item = tab.selected_row
                if isinstance(item, Contact):
                    jid = item.bare_jid
                else:
                    return self.core.information('No subscription to accept', 'Warning')
        else:
            jid = safeJID(args[0]).bare
        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 self.core.information('No subscription to accept', 'Warning')
        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)
        self.core.information('%s is now authorized' % jid, 'Roster')

558
    @deny_anonymous
559
    @command_args_parser.quoted(1)
560
    def add(self, args):
561 562 563 564 565
        """
        Add the specified JID to the roster, and automatically
        accept the reverse subscription
        """
        if args is None:
566 567 568 569
            tab = self.core.tabs.current_tab
            ConversationTab = tabs.ConversationTab
            if isinstance(tab, ConversationTab):
                jid = tab.general_jid
570 571 572 573
                if jid in roster and roster[jid].subscription in ('to', 'both'):
                    return self.core.information('Already subscribed.', 'Roster')
                roster.add(jid)
                roster.modified()
574 575 576
                return self.core.information('%s was added to the roster' % jid, 'Roster')
            else:
                return self.core.information('No JID specified', 'Error')
577 578 579 580 581 582 583 584 585 586
        jid = safeJID(safeJID(args[0]).bare)
        if not str(jid):
            self.core.information(
                'The provided JID (%s) is not valid' % (args[0], ), 'Error')
            return
        if jid in roster and roster[jid].subscription in ('to', 'both'):
            return self.core.information('Already subscribed.', 'Roster')
        roster.add(jid)
        roster.modified()
        self.core.information('%s was added to the roster' % jid, 'Roster')
587

588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637
    @deny_anonymous
    @command_args_parser.quoted(0, 1)
    def deny(self, args):
        """
        /deny [jid]
        Denies a JID from our roster
        """
        jid = None
        if not args:
            tab = self.core.tabs.current_tab
            if isinstance(tab, tabs.RosterInfoTab):
                item = tab.roster_win.selected_row
                if isinstance(item, Contact):
                    jid = item.bare_jid
        else:
            jid = safeJID(args[0]).bare
            if jid not in [jid for jid in roster.jids()]:
                jid = None
        if jid is None:
            self.core.information('No subscription to deny', 'Warning')
            return

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

    @deny_anonymous
    @command_args_parser.quoted(0, 1)
    def remove(self, args):
        """
        Remove the specified JID from the roster. i.e.: unsubscribe
        from its presence, and cancel its subscription to our.
        """
        jid = None
        if args:
            jid = safeJID(args[0]).bare
        else:
            tab = self.core.tabs.current_tab
            if isinstance(tab, tabs.RosterInfoTab):
                item = tab.roster_win.selected_row
                if isinstance(item, Contact):
                    jid = item.bare_jid
        if jid is None:
            self.core.information('No roster item to remove', 'Error')
            return
        roster.remove(jid)
        del roster[jid]

638 639 640 641 642 643 644 645
    @command_args_parser.ignored
    def command_reconnect(self):
        """
        /reconnect
        """
        if self.core.xmpp.is_connected():
            self.core.disconnect(reconnect=True)
        else:
646
            self.core.xmpp.start()
647

648
    @command_args_parser.quoted(0, 3)
649
    def set(self, args):
650 651 652 653 654 655 656 657
        """
        /set [module|][section] <option> [value]
        """
        if args is None or len(args) == 0:
            config_dict = config.to_dict()
            lines = []
            theme = get_theme()
            for section_name, section in config_dict.items():
mathieui's avatar
mathieui committed
658 659 660 661 662 663
                lines.append(
                    '\x19%(section_col)s}[%(section)s]\x19o' % {
                        'section': section_name,
                        'section_col': dump_tuple(
                            theme.COLOR_INFORMATION_TEXT),
                    })
664
                for option_name, option_value in section.items():
665 666
                    if isinstance(option_name, str) and \
                        'password' in option_name and 'eval_password' not in option_name:
667
                        option_value = '********'
mathieui's avatar
mathieui committed
668 669 670 671
                    lines.append(
                        '%s\x19%s}=\x19o%s' %
                        (option_name, dump_tuple(
                            theme.COLOR_REVISIONS_MESSAGE), option_value))
672 673 674 675
            info = ('Current  options:\n%s' % '\n'.join(lines), 'Info')
        elif len(args) == 1:
            option = args[0]
            value = config.get(option)
676 677
            if isinstance(option, str) and \
                'password' in option and 'eval_password' not in option and value is not None:
678
                value = '********'
679 680
            if value is None and '=' in option:
                args = option.split('=', 1)
681
            info = ('%s=%s' % (option, value), 'Info')
682 683 684 685 686
        if len(args) == 2:
            if '|' in args[0]:
                plugin_name, section = args[0].split('|')[:2]
                if not section:
                    section = plugin_name
687
                option = args[1]
688
                if plugin_name not in self.core.plugin_manager.plugins:
mathieui's avatar
mathieui committed
689 690
                    file_name = self.core.plugin_manager.plugins_conf_dir / (
                        plugin_name + '.cfg')
691 692
                    plugin_config = PluginConfig(file_name, plugin_name)
                else:
mathieui's avatar
mathieui committed
693 694
                    plugin_config = self.core.plugin_manager.plugins[
                        plugin_name].config
695
                value = plugin_config.get(option, default='', section=section)
696 697
                info = ('%s=%s' % (option, value), 'Info')
            else:
698 699 700 701 702 703 704 705 706 707
                possible_section = args[0]
                if config.has_section(possible_section):
                    section = possible_section
                    option = args[1]
                    value = config.get(option, section=section)
                    info = ('%s=%s' % (option, value), 'Info')
                else:
                    option = args[0]
                    value = args[1]
                    info = config.set_and_save(option, value)
708
                    self.core.trigger_configuration_change(option, value)
709 710 711 712 713 714 715
        elif len(args) == 3:
            if '|' in args[0]:
                plugin_name, section = args[0].split('|')[:2]
                if not section:
                    section = plugin_name
                option = args[1]
                value = args[2]
716
                if plugin_name not in self.core.plugin_manager.plugins:
mathieui's avatar
mathieui committed
717 718
                    file_name = self.core.plugin_manager.plugins_conf_dir / (
                        plugin_name + '.cfg')
719 720
                    plugin_config = PluginConfig(file_name, plugin_name)
                else:
mathieui's avatar
mathieui committed
721 722
                    plugin_config = self.core.plugin_manager.plugins[
                        plugin_name].config
723 724 725
                info = plugin_config.set_and_save(option, value, section)
            else:
                if args[0] == '.':
726
                    name = self.core.tabs.current_tab.jid.bare
727
                    if not name:
mathieui's avatar
mathieui committed
728 729
                        self.core.information(
                            'Invalid tab to use the "." argument.', 'Error')
730 731 732 733 734 735
                        return
                    section = name
                else:
                    section = args[0]
                option = args[1]
                value = args[2]
Madhur Garg's avatar
Madhur Garg committed
736
                info = config.set_and_save(option, value, section)
737
                self.core.trigger_configuration_change(option, value)
738
        elif len(args) > 3:
739 740
            return self.help('set')
        self.core.information(*info)
741 742

    @command_args_parser.quoted(1, 2)
743
    def set_default(self, args):
744 745 746 747 748 749 750 751
        """
        /set_default [section] <option>
        """
        if len(args) == 1:
            option = args[0]
            section = 'Poezio'
        elif len(args) == 2:
            section = args[0]
mathieui's avatar
mathieui committed
752 753
            option = args[1]
        else:
754
            return self.help('set_default')
755 756 757 758

        default_config = DEFAULT_CONFIG.get(section, tuple())
        if option not in default_config:
            info = ("Option %s has no default value" % (option), "Error")
759 760
            return self.core.information(*info)
        self.set('%s %s %s' % (section, option, default_config[option]))
761 762

    @command_args_parser.quoted(1)
763
    def toggle(self, args):
764 765 766 767 768
        """
        /toggle <option>
        shortcut for /set <option> toggle
        """
        if args is None:
769
            return self.help('toggle')
770 771

        if args[0]:
772
            self.set('%s toggle' % args[0])
773 774

    @command_args_parser.quoted(1, 1)
775
    def server_cycle(self, args):
776 777
        """
        Do a /cycle on each room of the given server.
778
        If none, do it on the server of the current tab
779
        """
780
        tab = self.core.tabs.current_tab
781 782
        message = ""
        if args:
783 784 785 786 787 788 789
            try:
                domain = JID(args[0]).domain
            except InvalidJID:
                return self.core.information(
                    "Invalid server domain: %s" % args[0],
                    "Error"
                )
790 791
            if len(args) == 2:
                message = args[1]
mathieui's avatar
mathieui committed
792
        else:
793
            if isinstance(tab, tabs.MucTab):
794
                domain = tab.jid.domain
mathieui's avatar
mathieui committed
795
            else:
796 797
                return self.core.information("No server specified", "Error")
        for tab in self.core.get_tabs(tabs.MucTab):
798
            if tab.jid.domain == domain:
mathieui's avatar
mathieui committed
799 800
                tab.leave_room(message)
                tab.join()
801 802

    @command_args_parser.quoted(1)
803
    def last_activity(self, args):
804 805 806
        """
        /last_activity <jid>
        """
mathieui's avatar
mathieui committed
807

808 809 810 811
        def callback(iq):
            "Callback for the last activity"
            if iq['type'] != 'result':
                if iq['error']['type'] == 'auth':
mathieui's avatar
mathieui committed
812 813 814
                    self.core.information(
                        'You are not allowed to see the '
                        'activity of this contact.', 'Error')
815
                else:
mathieui's avatar
mathieui committed
816 817
                    self.core.information('Error retrieving the activity',
                                          'Error')
818 819 820 821 822 823
                return
            seconds = iq['last_activity']['seconds']
            status = iq['last_activity']['status']
            from_ = iq['from']
            if not safeJID(from_).user:
                msg = 'The uptime of %s is %s.' % (
mathieui's avatar
mathieui committed
824
                    from_, common.parse_secs_to_str(seconds))
mathieui's avatar
mathieui committed
825
            else:
826
                msg = 'The last activity of %s was %s ago%s' % (
mathieui's avatar
mathieui committed
827
                    from_, common.parse_secs_to_str(seconds),
Kim Alvefur's avatar
Kim Alvefur committed
828
                    (' and their last status was %s' % status)
mathieui's avatar
mathieui committed
829
                    if status else '')
830
            self.core.information(msg, 'Info')
831 832

        if args is None:
833
            return self.help('last_activity')
834
        jid = safeJID(args[0])
mathieui's avatar
mathieui committed
835 836
        self.core.xmpp.plugin['xep_0012'].get_last_activity(
            jid, callback=callback)
837 838

    @command_args_parser.quoted(0, 2)
839
    def mood(self, args):
840 841 842 843
        """
        /mood [<mood> [text]]
        """
        if not args:
844
            return self.core.xmpp.plugin['xep_0107'].stop()
845 846 847

        mood = args[0]
        if mood not in pep.MOODS:
mathieui's avatar
mathieui committed
848 849
            return self.core.information(
                '%s is not a correct value for a mood.' % mood, 'Error')
850 851
        if len(args) == 2:
            text = args[1]
mathieui's avatar
mathieui committed
852
        else:
853
            text = None
mathieui's avatar
mathieui committed
854 855
        self.core.xmpp.plugin['xep_0107'].publish_mood(
            mood, text, callback=dumb_callback)
856 857

    @command_args_parser.quoted(0, 3)
858
    def activity(self, args):
859 860 861 862 863
        """
        /activity [<general> [specific] [text]]
        """
        length = len(args)
        if not length:
864
            return self.core.xmpp.plugin['xep_0108'].stop()
865 866 867

        general = args[0]
        if general not in pep.ACTIVITIES:
mathieui's avatar
mathieui committed
868 869
            return self.core.information(
                '%s is not a correct value for an activity' % general, 'Error')
870
        specific = None
mathieui's avatar
mathieui committed
871
        text = None
872 873 874 875 876 877
        if length == 2:
            if args[1] in pep.ACTIVITIES[general]:
                specific = args[1]
            else:
                text = args[1]
        elif length == 3:
mathieui's avatar
mathieui committed
878
            specific = args[1]
879 880
            text = args[2]
        if specific and specific not in pep.ACTIVITIES[general]:
mathieui's avatar
mathieui committed
881 882