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

import logging

log = logging.getLogger(__name__)

9
import asyncio
mathieui's avatar
mathieui committed
10 11
from xml.etree import cElementTree as ET

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

19 20 21 22 23 24 25 26 27 28 29
from poezio import common
from poezio import pep
from poezio import tabs
from poezio.bookmarks import Bookmark
from poezio.common import safeJID
from poezio.config import config, DEFAULT_CONFIG, options as config_opts
from poezio import multiuserchat as muc
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

mathieui's avatar
mathieui committed
31
from poezio.core.structs import Command, POSSIBLE_SHOW
mathieui's avatar
mathieui committed
32 33


34 35 36 37 38
class CommandCore:
    def __init__(self, core):
        self.core = core

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

72
            tab_commands = self.core.tabs.current_tab.commands
73 74 75 76
            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
77
            else:
78
                self.core.information('Unknown command: %s' % command, 'Error')
79 80 81 82
                return
            if isinstance(tup, Command):
                msg = 'Usage: /%s %s\n' % (command, tup.usage)
                msg += tup.desc
mathieui's avatar
mathieui committed
83
            else:
84
                msg = tup[1]
85
        self.core.information(msg, 'Help')
86 87

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

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

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

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

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

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

124
        pres = self.core.xmpp.make_presence()
125 126 127
        if msg:
            pres['status'] = msg
        pres['type'] = show
128
        self.core.events.trigger('send_normal_presence', pres)
mathieui's avatar
mathieui committed
129
        pres.send()
130
        current = self.core.tabs.current_tab
131 132 133
        is_muctab = isinstance(current, tabs.MucTab)
        if is_muctab and current.joined and show in ('away', 'xa'):
            current.send_chat_state('inactive')
134
        for tab in self.core.tabs:
135
            if isinstance(tab, tabs.MucTab) and tab.joined:
mathieui's avatar
mathieui committed
136 137
                muc.change_show(self.core.xmpp, tab.name, tab.own_nick, show,
                                msg)
138 139
            if hasattr(tab, 'directed_presence'):
                del tab.directed_presence
140
        self.core.set_status(show, msg)
141 142 143 144
        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])
145
    def presence(self, args):
146 147 148 149
        """
        /presence <JID> [type] [status]
        """
        if args is None:
150
            return self.help('presence')
151

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

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

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

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

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

228
        current_tab = self.core.tabs.current_tab
229 230 231 232 233 234 235 236 237 238 239 240
        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
241
                for tab in self.core.tabs:
242 243 244
                    if not old_tab and value == tab.name:
                        old_tab = tab
                if not old_tab:
mathieui's avatar
mathieui committed
245 246
                    self.core.information("Tab %s does not exist" % args[0],
                                          "Error")
247 248 249
                    return None
                ref = old_tab.nb
            return ref
mathieui's avatar
mathieui committed
250

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

    @command_args_parser.quoted(0, 1)
261
    def list(self, args):
262 263 264 265 266
        """
        /list [server]
        Opens a MucListTab containing the list of the room in the specified server
        """
        if args is None:
267
            return self.help('list')
268 269 270
        elif args:
            jid = safeJID(args[0])
        else:
271
            if not isinstance(self.core.tabs.current_tab, tabs.MucTab):
mathieui's avatar
mathieui committed
272 273
                return self.core.information('Please provide a server',
                                             'Error')
274
            jid = safeJID(self.core.tabs.current_tab.name)
275
        list_tab = tabs.MucListTab(self.core, jid)
276
        self.core.add_tab(list_tab, True)
277
        cb = list_tab.on_muc_list_item_received
mathieui's avatar
mathieui committed
278
        self.core.xmpp.plugin['xep_0030'].get_items(jid=jid, callback=cb)
279 280

    @command_args_parser.quoted(1)
281
    def version(self, args):
282 283 284 285
        """
        /version <jid>
        """
        if args is None:
286
            return self.help('version')
287
        jid = safeJID(args[0])
288
        if jid.resource or jid not in roster or not roster[jid].resources:
289 290
            self.core.xmpp.plugin['xep_0092'].get_version(
                jid, callback=self.core.handler.on_version_result)
291 292
        elif jid in roster:
            for resource in roster[jid].resources:
293 294
                self.core.xmpp.plugin['xep_0092'].get_version(
                    resource.jid, callback=self.core.handler.on_version_result)
295

mathieui's avatar
mathieui committed
296
    def _empty_join(self):
297
        tab = self.core.tabs.current_tab
mathieui's avatar
mathieui committed
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
        if not isinstance(tab, (tabs.MucTab, tabs.PrivateTab)):
            return (None, None)
        room = safeJID(tab.name).bare
        nick = tab.own_nick
        return (room, nick)

    def _parse_join_jid(self, jid_string):
        # we try to join a server directly
        if jid_string.startswith('@'):
            server_root = True
            info = safeJID(jid_string[1:])
        else:
            info = safeJID(jid_string)
            server_root = False

        set_nick = ''
        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 == '':
321
            tab = self.core.tabs.current_tab
mathieui's avatar
mathieui committed
322 323 324 325 326 327 328 329 330 331 332 333
            if not isinstance(tab, tabs.MucTab):
                room, set_nick = (None, None)
            else:
                room = tab.name
                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:
334
                tab = self.core.tabs.current_tab
mathieui's avatar
mathieui committed
335 336 337 338 339 340
                if isinstance(tab, tabs.MucTab):
                    if tab.name.find('@') != -1:
                        domain = safeJID(tab.name).domain
                        room += '@%s' % domain
        return (room, set_nick)

341
    @command_args_parser.quoted(0, 2)
342
    def join(self, args):
343 344 345 346
        """
        /join [room][/nick] [password]
        """
        if len(args) == 0:
mathieui's avatar
mathieui committed
347
            room, nick = self._empty_join()
mathieui's avatar
mathieui committed
348
        else:
mathieui's avatar
mathieui committed
349 350
            room, nick = self._parse_join_jid(args[0])
        if not room and not nick:
mathieui's avatar
mathieui committed
351
            return  # nothing was parsed
mathieui's avatar
mathieui committed
352

353
        room = room.lower()
mathieui's avatar
mathieui committed
354 355 356 357 358 359 360 361 362
        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)

363 364
        if room in self.core.pending_invites:
            del self.core.pending_invites[room]
mathieui's avatar
mathieui committed
365

366
        tab = self.core.tabs.by_name_and_class(room, tabs.MucTab)
mathieui's avatar
mathieui committed
367 368 369 370 371
        # New tab
        if tab is None:
            tab = self.core.open_new_room(room, nick, password=password)
            tab.join()
        else:
372
            self.core.focus_tab(tab)
373
            if tab.own_nick == nick and tab.joined:
374
                self.core.information('/join: Nothing to do.', 'Info')
375 376 377 378
            else:
                tab.command_part('')
                tab.own_nick = nick
                tab.password = password
mathieui's avatar
mathieui committed
379
                tab.join()
mathieui's avatar
mathieui committed
380

381
        if config.get('bookmark_on_join'):
mathieui's avatar
mathieui committed
382 383
            method = 'remote' if config.get(
                'use_remote_bookmarks') else 'local'
384 385
            self._add_bookmark('%s/%s' % (room, nick), True, password, method)

386
        if tab == self.core.tabs.current_tab:
mathieui's avatar
mathieui committed
387 388
            tab.refresh()
            self.core.doupdate()
389 390

    @command_args_parser.quoted(0, 2)
391
    def bookmark_local(self, args):
392 393 394
        """
        /bookmark_local [room][/nick] [password]
        """
395 396
        if not args and not isinstance(self.core.tabs.current_tab,
                                       tabs.MucTab):
397 398 399
            return
        password = args[1] if len(args) > 1 else None
        jid = args[0] if args else None
mathieui's avatar
mathieui committed
400

401
        self._add_bookmark(jid, True, password, 'local')
402 403

    @command_args_parser.quoted(0, 3)
404
    def bookmark(self, args):
405 406 407
        """
        /bookmark [room][/nick] [autojoin] [password]
        """
408 409
        if not args and not isinstance(self.core.tabs.current_tab,
                                       tabs.MucTab):
410 411 412 413 414
            return
        jid = args[0] if args else ''
        password = args[2] if len(args) > 2 else None

        if not config.get('use_remote_bookmarks'):
415
            return self._add_bookmark(jid, True, password, 'local')
416 417 418

        if len(args) > 1:
            autojoin = False if args[1].lower() != 'true' else True
419
        else:
420 421
            autojoin = True

422
        self._add_bookmark(jid, autojoin, password, 'remote')
423 424 425 426

    def _add_bookmark(self, jid, autojoin, password, method):
        nick = None
        if not jid:
427
            tab = self.core.tabs.current_tab
428
            roomname = tab.name
429
            if tab.joined and tab.own_nick != self.core.own_nick:
430 431 432 433
                nick = tab.own_nick
            if password is None and tab.password is not None:
                password = tab.password
        elif jid == '*':
434
            return self._add_wildcard_bookmarks(method)
435
        else:
436 437 438
            info = safeJID(jid)
            roomname, nick = info.bare, info.resource
            if roomname == '':
439
                tab = self.core.tabs.current_tab
440
                if not isinstance(tab, tabs.MucTab):
441
                    return
442 443
                roomname = tab.name
        bookmark = self.core.bookmarks[roomname]
444 445
        if bookmark is None:
            bookmark = Bookmark(roomname)
446
            self.core.bookmarks.append(bookmark)
447 448 449 450 451 452
        bookmark.method = method
        bookmark.autojoin = autojoin
        if nick:
            bookmark.nick = nick
        if password:
            bookmark.password = password
mathieui's avatar
mathieui committed
453

454
        self.core.bookmarks.save_local()
455 456
        self.core.bookmarks.save_remote(self.core.xmpp,
                                        self.core.handler.on_bookmark_result)
457 458 459

    def _add_wildcard_bookmarks(self, method):
        new_bookmarks = []
460 461
        for tab in self.core.get_tabs(tabs.MucTab):
            bookmark = self.core.bookmarks[tab.name]
462
            if not bookmark:
mathieui's avatar
mathieui committed
463
                bookmark = Bookmark(tab.name, autojoin=True, method=method)
464 465 466 467
                new_bookmarks.append(bookmark)
            else:
                bookmark.method = method
                new_bookmarks.append(bookmark)
468 469 470 471
                self.core.bookmarks.remove(bookmark)
        new_bookmarks.extend(self.core.bookmarks.bookmarks)
        self.core.bookmarks.set(new_bookmarks)
        self.core.bookmarks.save_local()
472 473
        self.core.bookmarks.save_remote(self.core.xmpp,
                                        self.core.handler.on_bookmark_result)
474 475

    @command_args_parser.ignored
476
    def bookmarks(self):
477
        """/bookmarks"""
478 479
        tab = self.core.tabs.by_name_and_class('Bookmarks', tabs.BookmarksTab)
        old_tab = self.core.tabs.current_tab
480
        if tab:
481
            self.core.tabs.set_current_tab(tab)
mathieui's avatar
mathieui committed
482
        else:
483
            tab = tabs.BookmarksTab(self.core, self.core.bookmarks)
484
            self.core.tabs.append(tab)
485
            self.core.tabs.set_current_tab(tab)
486 487

    @command_args_parser.quoted(0, 1)
488
    def remove_bookmark(self, args):
489 490 491 492
        """/remove_bookmark [jid]"""

        def cb(success):
            if success:
493
                self.core.information('Bookmark deleted', 'Info')
494
            else:
mathieui's avatar
mathieui committed
495 496
                self.core.information('Error while deleting the bookmark',
                                      'Error')
mathieui's avatar
mathieui committed
497

498
        if not args:
499
            tab = self.core.tabs.current_tab
500 501 502
            if isinstance(tab, tabs.MucTab) and self.core.bookmarks[tab.name]:
                self.core.bookmarks.remove(tab.name)
                self.core.bookmarks.save(self.core.xmpp, callback=cb)
503
            else:
504
                self.core.information('No bookmark to remove', 'Info')
mathieui's avatar
mathieui committed
505
        else:
506 507 508
            if self.core.bookmarks[args[0]]:
                self.core.bookmarks.remove(args[0])
                self.core.bookmarks.save(self.core.xmpp, callback=cb)
509
            else:
510
                self.core.information('No bookmark to remove', 'Info')
511 512

    @command_args_parser.quoted(0, 3)
513
    def set(self, args):
514 515 516 517 518 519 520 521
        """
        /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
522 523 524 525 526 527
                lines.append(
                    '\x19%(section_col)s}[%(section)s]\x19o' % {
                        'section': section_name,
                        'section_col': dump_tuple(
                            theme.COLOR_INFORMATION_TEXT),
                    })
528
                for option_name, option_value in section.items():
529
                    if 'password' in option_name and 'eval_password' not in option_name:
530
                        option_value = '********'
mathieui's avatar
mathieui committed
531 532 533 534
                    lines.append(
                        '%s\x19%s}=\x19o%s' %
                        (option_name, dump_tuple(
                            theme.COLOR_REVISIONS_MESSAGE), option_value))
535 536 537 538
            info = ('Current  options:\n%s' % '\n'.join(lines), 'Info')
        elif len(args) == 1:
            option = args[0]
            value = config.get(option)
539
            if 'password' in option and 'eval_password' not in option and value is not None:
540
                value = '********'
541 542
            if value is None and '=' in option:
                args = option.split('=', 1)
543
            info = ('%s=%s' % (option, value), 'Info')
544 545 546 547 548
        if len(args) == 2:
            if '|' in args[0]:
                plugin_name, section = args[0].split('|')[:2]
                if not section:
                    section = plugin_name
549
                option = args[1]
550
                if plugin_name not in self.core.plugin_manager.plugins:
mathieui's avatar
mathieui committed
551 552
                    file_name = self.core.plugin_manager.plugins_conf_dir / (
                        plugin_name + '.cfg')
553 554
                    plugin_config = PluginConfig(file_name, plugin_name)
                else:
mathieui's avatar
mathieui committed
555 556
                    plugin_config = self.core.plugin_manager.plugins[
                        plugin_name].config
557
                value = plugin_config.get(option, default='', section=section)
558 559
                info = ('%s=%s' % (option, value), 'Info')
            else:
560 561 562 563 564 565 566 567 568 569
                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)
570
                    self.core.trigger_configuration_change(option, value)
571 572 573 574 575 576 577
        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]
578
                if plugin_name not in self.core.plugin_manager.plugins:
mathieui's avatar
mathieui committed
579 580
                    file_name = self.core.plugin_manager.plugins_conf_dir / (
                        plugin_name + '.cfg')
581 582
                    plugin_config = PluginConfig(file_name, plugin_name)
                else:
mathieui's avatar
mathieui committed
583 584
                    plugin_config = self.core.plugin_manager.plugins[
                        plugin_name].config
585 586 587
                info = plugin_config.set_and_save(option, value, section)
            else:
                if args[0] == '.':
588
                    name = safeJID(self.core.tabs.current_tab.name).bare
589
                    if not name:
mathieui's avatar
mathieui committed
590 591
                        self.core.information(
                            'Invalid tab to use the "." argument.', 'Error')
592 593 594 595 596 597
                        return
                    section = name
                else:
                    section = args[0]
                option = args[1]
                value = args[2]
Madhur Garg's avatar
Madhur Garg committed
598
                info = config.set_and_save(option, value, section)
599
                self.core.trigger_configuration_change(option, value)
600
        elif len(args) > 3:
601 602
            return self.help('set')
        self.core.information(*info)
603 604

    @command_args_parser.quoted(1, 2)
605
    def set_default(self, args):
606 607 608 609 610 611 612 613
        """
        /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
614 615
            option = args[1]
        else:
616
            return self.help('set_default')
617 618 619 620

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

    @command_args_parser.quoted(1)
625
    def toggle(self, args):
626 627 628 629 630
        """
        /toggle <option>
        shortcut for /set <option> toggle
        """
        if args is None:
631
            return self.help('toggle')
632 633

        if args[0]:
634
            self.set('%s toggle' % args[0])
635 636

    @command_args_parser.quoted(1, 1)
637
    def server_cycle(self, args):
638 639
        """
        Do a /cycle on each room of the given server.
640
        If none, do it on the server of the current tab
641
        """
642
        tab = self.core.tabs.current_tab
643 644
        message = ""
        if args:
645 646 647 648 649 650 651
            try:
                domain = JID(args[0]).domain
            except InvalidJID:
                return self.core.information(
                    "Invalid server domain: %s" % args[0],
                    "Error"
                )
652 653
            if len(args) == 2:
                message = args[1]
mathieui's avatar
mathieui committed
654
        else:
655 656
            if isinstance(tab, tabs.MucTab):
                domain = safeJID(tab.name).domain
mathieui's avatar
mathieui committed
657
            else:
658 659
                return self.core.information("No server specified", "Error")
        for tab in self.core.get_tabs(tabs.MucTab):
660
            if JID(tab.name).domain == domain:
mathieui's avatar
mathieui committed
661 662
                tab.leave_room(message)
                tab.join()
663 664

    @command_args_parser.quoted(1)
665
    def last_activity(self, args):
666 667 668
        """
        /last_activity <jid>
        """
mathieui's avatar
mathieui committed
669

670 671 672 673
        def callback(iq):
            "Callback for the last activity"
            if iq['type'] != 'result':
                if iq['error']['type'] == 'auth':
mathieui's avatar
mathieui committed
674 675 676
                    self.core.information(
                        'You are not allowed to see the '
                        'activity of this contact.', 'Error')
677
                else:
mathieui's avatar
mathieui committed
678 679
                    self.core.information('Error retrieving the activity',
                                          'Error')
680 681 682 683 684 685
                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
686
                    from_, common.parse_secs_to_str(seconds))
mathieui's avatar
mathieui committed
687
            else:
688
                msg = 'The last activity of %s was %s ago%s' % (
mathieui's avatar
mathieui committed
689
                    from_, common.parse_secs_to_str(seconds),
Kim Alvefur's avatar
Kim Alvefur committed
690
                    (' and their last status was %s' % status)
mathieui's avatar
mathieui committed
691
                    if status else '')
692
            self.core.information(msg, 'Info')
693 694

        if args is None:
695
            return self.help('last_activity')
696
        jid = safeJID(args[0])
mathieui's avatar
mathieui committed
697 698
        self.core.xmpp.plugin['xep_0012'].get_last_activity(
            jid, callback=callback)
699 700

    @command_args_parser.quoted(0, 2)
701
    def mood(self, args):
702 703 704 705
        """
        /mood [<mood> [text]]
        """
        if not args:
706
            return self.core.xmpp.plugin['xep_0107'].stop()
707 708 709

        mood = args[0]
        if mood not in pep.MOODS:
mathieui's avatar
mathieui committed
710 711
            return self.core.information(
                '%s is not a correct value for a mood.' % mood, 'Error')
712 713
        if len(args) == 2:
            text = args[1]
mathieui's avatar
mathieui committed
714
        else:
715
            text = None
mathieui's avatar
mathieui committed
716 717
        self.core.xmpp.plugin['xep_0107'].publish_mood(
            mood, text, callback=dumb_callback)
718 719

    @command_args_parser.quoted(0, 3)
720
    def activity(self, args):
721 722 723 724 725
        """
        /activity [<general> [specific] [text]]
        """
        length = len(args)
        if not length:
726
            return self.core.xmpp.plugin['xep_0108'].stop()
727 728 729

        general = args[0]
        if general not in pep.ACTIVITIES:
mathieui's avatar
mathieui committed
730 731
            return self.core.information(
                '%s is not a correct value for an activity' % general, 'Error')
732
        specific = None
mathieui's avatar
mathieui committed
733
        text = None
734 735 736 737 738 739
        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
740
            specific = args[1]
741 742
            text = args[2]
        if specific and specific not in pep.ACTIVITIES[general]:
mathieui's avatar
mathieui committed
743 744 745
            return self.core.information(
                '%s is not a correct value '
                'for an activity' % specific, 'Error')
mathieui's avatar
mathieui committed
746 747
        self.core.xmpp.plugin['xep_0108'].publish_activity(
            general, specific, text, callback=dumb_callback)
748 749

    @command_args_parser.quoted(0, 2)
750
    def gaming(self, args):
751 752 753 754
        """
        /gaming [<game name> [server address]]
        """
        if not args:
755
            return self.core.xmpp.plugin['xep_0196'].stop()
756 757 758 759

        name = args[0]
        if len(args) > 1:
            address = args[1]
mathieui's avatar
mathieui committed
760
        else:
761
            address = None
mathieui's avatar
mathieui committed
762 763
        return self.core.xmpp.plugin['xep_0196'].publish_gaming(
            name=name, server_address=address, callback=dumb_callback)
764 765

    @command_args_parser.quoted(2, 1, [None])
766
    def invite(self, args):
767 768 769
        """/invite <to> <room> [reason]"""

        if args is None:
770
            return self.help('invite')
771 772 773 774

        reason = args[2]
        to = safeJID(args[0])
        room = safeJID(args[1]).bare
775 776
        self.core.invite(to.full, room, reason=reason)
        self.core.information('Invited %s to %s' % (to.bare, room), 'Info')
777

Maxime Buquet's avatar
Maxime Buquet committed
778
    @command_args_parser.quoted(1, 0)
779
    def impromptu(self, args: str) -> None:
Maxime Buquet's avatar
Maxime Buquet committed
780 781 782 783 784
        """/impromptu <jid> [<jid> ...]"""

        if args is None:
            return self.help('impromptu')

785 786 787 788 789
        jids = set()
        current_tab = self.core.tabs.current_tab
        if isinstance(current_tab, tabs.ConversationTab):
            jids.add(current_tab.general_jid)

Maxime Buquet's avatar
Maxime Buquet committed
790
        for jid in common.shell_split(' '.join(args)):
791
            jids.add(safeJID(jid).bare)
Maxime Buquet's avatar
Maxime Buquet committed
792

793
        asyncio.ensure_future(self.core.impromptu(jids))
794
        self.core.information('Invited %s to a random room' % (', '.join(jids)), 'Info')
Maxime Buquet's avatar
Maxime Buquet committed
795

796
    @command_args_parser.quoted(1, 1, [''])
797
    def decline(self, args):
798 799
        """/decline <room@server.tld> [reason]"""
        if args is None:
800
            return self.help('decline')
801
        jid = safeJID(args[0])
802
        if jid.bare not in self.core.pending_invites:
803 804
            return
        reason = args[1]
805
        del self.core.pending_invites[jid.bare]
mathieui's avatar
mathieui committed
806 807 808
        self.core.xmpp.plugin['xep_0045'].decline_invite(
            jid.bare, self.core.pending_invites[jid.bare], reason)

mathieui's avatar
mathieui committed
809 810 811

### Commands without a completion in this class ###

812
    @command_args_parser.ignored
813
    def invitations(self):
814 815
        """/invitations"""
        build = ""
816
        for invite in self.core.pending_invites:
mathieui's avatar
mathieui committed
817 818
            build += "%s by %s" % (
                invite, safeJID(self.core.pending_invites[invite]).bare)
819
        if self.core.pending_invites:
820 821 822
            build = "You are invited to the following rooms:\n" + build
        else:
            build = "You do not have any pending invitations."
823
        self.core.information(build, 'Info')
824 825

    @command_args_parser.quoted(0, 1, [None])
826
    def quit(self, args):
827 828 829
        """
        /quit [message]
        """
830 831
        if not self.core.xmpp.is_connected():
            self.core.exit()
832 833 834 835
            return

        msg = args[0]
        if config.get('enable_user_mood'):
836
            self.core.xmpp.plugin['xep_0107'].stop()
837
        if config.get('enable_user_activity'):
838
            self.core.xmpp.plugin['xep_0108'].stop()
839
        if config.get('enable_user_gaming'):
840 841 842 843
            self.core.xmpp.plugin['xep_0196'].stop()
        self.core.save_config()
        self.core.plugin_manager.disable_plugins()
        self.core.disconnect(msg)
mathieui's avatar
mathieui committed
844 845
        self.core.xmpp.add_event_handler(
            "disconnected", self.core.exit, disposable=True)
846 847

    @command_args_parser.quoted(0, 1, [''])
848
    def destroy_room(self, args):
849 850 851 852 853
        """
        /destroy_room [JID]
        """
        room = safeJID(args[0]).bare
        if room: