commands.py 35 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 13
from slixmpp.exceptions import XMPPError
from slixmpp.xmlstream.xmlstream import NotConnectedError
louiz’'s avatar
louiz’ committed
14 15 16
from slixmpp.xmlstream.stanzabase import StanzaBase
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
mathieui's avatar
mathieui committed
17

18 19 20 21 22 23 24 25 26 27 28
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
29

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

mathieui's avatar
mathieui committed
295
    def _empty_join(self):
296
        tab = self.core.tabs.current_tab
mathieui's avatar
mathieui committed
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
        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 == '':
320
            tab = self.core.tabs.current_tab
mathieui's avatar
mathieui committed
321 322 323 324 325 326 327 328 329 330 331 332
            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:
333
                tab = self.core.tabs.current_tab
mathieui's avatar
mathieui committed
334 335 336 337 338 339
                if isinstance(tab, tabs.MucTab):
                    if tab.name.find('@') != -1:
                        domain = safeJID(tab.name).domain
                        room += '@%s' % domain
        return (room, set_nick)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

497
        if not args:
498
            tab = self.core.tabs.current_tab
499 500 501
            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)
502
            else:
503
                self.core.information('No bookmark to remove', 'Info')
mathieui's avatar
mathieui committed
504
        else:
505 506 507
            if self.core.bookmarks[args[0]]:
                self.core.bookmarks.remove(args[0])
                self.core.bookmarks.save(self.core.xmpp, callback=cb)
508
            else:
509
                self.core.information('No bookmark to remove', 'Info')
510 511

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

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

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

    @command_args_parser.quoted(1)
620
    def toggle(self, args):
621 622 623 624 625
        """
        /toggle <option>
        shortcut for /set <option> toggle
        """
        if args is None:
626
            return self.help('toggle')
627 628

        if args[0]:
629
            self.set('%s toggle' % args[0])
630 631

    @command_args_parser.quoted(1, 1)
632
    def server_cycle(self, args):
633 634 635 636
        """
        Do a /cycle on each room of the given server.
        If none, do it on the current tab
        """
637
        tab = self.core.tabs.current_tab
638 639 640 641 642
        message = ""
        if args:
            domain = args[0]
            if len(args) == 2:
                message = args[1]
mathieui's avatar
mathieui committed
643
        else:
644 645
            if isinstance(tab, tabs.MucTab):
                domain = safeJID(tab.name).domain
mathieui's avatar
mathieui committed
646
            else:
647 648
                return self.core.information("No server specified", "Error")
        for tab in self.core.get_tabs(tabs.MucTab):
649
            if tab.name.endswith(domain):
mathieui's avatar
mathieui committed
650 651
                tab.leave_room(message)
                tab.join()
652 653

    @command_args_parser.quoted(1)
654
    def last_activity(self, args):
655 656 657
        """
        /last_activity <jid>
        """
mathieui's avatar
mathieui committed
658

659 660 661 662
        def callback(iq):
            "Callback for the last activity"
            if iq['type'] != 'result':
                if iq['error']['type'] == 'auth':
mathieui's avatar
mathieui committed
663 664 665
                    self.core.information(
                        'You are not allowed to see the '
                        'activity of this contact.', 'Error')
666
                else:
mathieui's avatar
mathieui committed
667 668
                    self.core.information('Error retrieving the activity',
                                          'Error')
669 670 671 672 673 674
                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
675
                    from_, common.parse_secs_to_str(seconds))
mathieui's avatar
mathieui committed
676
            else:
677
                msg = 'The last activity of %s was %s ago%s' % (
mathieui's avatar
mathieui committed
678 679 680
                    from_, common.parse_secs_to_str(seconds),
                    (' and his/her last status was %s' % status)
                    if status else '')
681
            self.core.information(msg, 'Info')
682 683

        if args is None:
684
            return self.help('last_activity')
685
        jid = safeJID(args[0])
mathieui's avatar
mathieui committed
686 687
        self.core.xmpp.plugin['xep_0012'].get_last_activity(
            jid, callback=callback)
688 689

    @command_args_parser.quoted(0, 2)
690
    def mood(self, args):
691 692 693 694
        """
        /mood [<mood> [text]]
        """
        if not args:
695
            return self.core.xmpp.plugin['xep_0107'].stop()
696 697 698

        mood = args[0]
        if mood not in pep.MOODS:
mathieui's avatar
mathieui committed
699 700
            return self.core.information(
                '%s is not a correct value for a mood.' % mood, 'Error')
701 702
        if len(args) == 2:
            text = args[1]
mathieui's avatar
mathieui committed
703
        else:
704
            text = None
mathieui's avatar
mathieui committed
705 706
        self.core.xmpp.plugin['xep_0107'].publish_mood(
            mood, text, callback=dumb_callback)
707 708

    @command_args_parser.quoted(0, 3)
709
    def activity(self, args):
710 711 712 713 714
        """
        /activity [<general> [specific] [text]]
        """
        length = len(args)
        if not length:
715
            return self.core.xmpp.plugin['xep_0108'].stop()
716 717 718

        general = args[0]
        if general not in pep.ACTIVITIES:
mathieui's avatar
mathieui committed
719 720
            return self.core.information(
                '%s is not a correct value for an activity' % general, 'Error')
721
        specific = None
mathieui's avatar
mathieui committed
722
        text = None
723 724 725 726 727 728
        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
729
            specific = args[1]
730 731
            text = args[2]
        if specific and specific not in pep.ACTIVITIES[general]:
mathieui's avatar
mathieui committed
732 733 734
            return self.core.information(
                '%s is not a correct value '
                'for an activity' % specific, 'Error')
mathieui's avatar
mathieui committed
735 736
        self.core.xmpp.plugin['xep_0108'].publish_activity(
            general, specific, text, callback=dumb_callback)
737 738

    @command_args_parser.quoted(0, 2)
739
    def gaming(self, args):
740 741 742 743
        """
        /gaming [<game name> [server address]]
        """
        if not args:
744
            return self.core.xmpp.plugin['xep_0196'].stop()
745 746 747 748

        name = args[0]
        if len(args) > 1:
            address = args[1]
mathieui's avatar
mathieui committed
749
        else:
750
            address = None
mathieui's avatar
mathieui committed
751 752
        return self.core.xmpp.plugin['xep_0196'].publish_gaming(
            name=name, server_address=address, callback=dumb_callback)
753 754

    @command_args_parser.quoted(2, 1, [None])
755
    def invite(self, args):
756 757 758
        """/invite <to> <room> [reason]"""

        if args is None:
759
            return self.help('invite')
760 761 762 763

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

Maxime Buquet's avatar
Maxime Buquet committed
767
    @command_args_parser.quoted(1, 0)
768
    def impromptu(self, args: str) -> None:
Maxime Buquet's avatar
Maxime Buquet committed
769 770 771 772 773
        """/impromptu <jid> [<jid> ...]"""

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

774 775 776 777 778
        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
779
        for jid in common.shell_split(' '.join(args)):
780
            jids.add(safeJID(jid).bare)
Maxime Buquet's avatar
Maxime Buquet committed
781

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

785
    @command_args_parser.quoted(1, 1, [''])
786
    def decline(self, args):
787 788
        """/decline <room@server.tld> [reason]"""
        if args is None:
789
            return self.help('decline')
790
        jid = safeJID(args[0])
791
        if jid.bare not in self.core.pending_invites:
792 793
            return
        reason = args[1]
794
        del self.core.pending_invites[jid.bare]
mathieui's avatar
mathieui committed
795 796 797
        self.core.xmpp.plugin['xep_0045'].decline_invite(
            jid.bare, self.core.pending_invites[jid.bare], reason)

mathieui's avatar
mathieui committed
798 799 800

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

801
    @command_args_parser.ignored
802
    def invitations(self):
803 804
        """/invitations"""
        build = ""
805
        for invite in self.core.pending_invites:
mathieui's avatar
mathieui committed
806 807
            build += "%s by %s" % (
                invite, safeJID(self.core.pending_invites[invite]).bare)
808
        if self.core.pending_invites:
809 810 811
            build = "You are invited to the following rooms:\n" + build
        else:
            build = "You do not have any pending invitations."
812
        self.core.information(build, 'Info')
813 814

    @command_args_parser.quoted(0, 1, [None])
815
    def quit(self, args):
816 817 818
        """
        /quit [message]
        """
819 820
        if not self.core.xmpp.is_connected():
            self.core.exit()
821 822 823 824
            return

        msg = args[0]
        if config.get('enable_user_mood'):
825
            self.core.xmpp.plugin['xep_0107'].stop()
826
        if config.get('enable_user_activity'):
827
            self.core.xmpp.plugin['xep_0108'].stop()
828
        if config.get('enable_user_gaming'):
829 830 831 832
            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
833 834
        self.core.xmpp.add_event_handler(
            "disconnected", self.core.exit, disposable=True)
835 836

    @command_args_parser.quoted(0, 1, [''])
837
    def destroy_room(self, args):
838 839 840 841 842
        """
        /destroy_room [JID]
        """
        room = safeJID(args[0]).bare
        if room:
843
            muc.destroy_room(self.core.xmpp, room)
844 845
        elif isinstance(self.core.tabs.current_tab,
                        tabs.MucTab) and not args[0]:
mathieui's avatar
mathieui committed
846
            muc.destroy_room(self.core.xmpp,
847
                             self.core.tabs.current_tab.general_jid)
848
        else:
849
            self.core.information('Invalid JID: "%s"' % args[0], 'Error')
850 851

    @command_args_parser.quoted(1, 1, [''])
852
    def bind(self, args):
853 854 855 856
        """
        Bind a key.
        """
        if args is None:
857
            return self.help('bind')
858 859

        if not config.silent_set(args[0], args[1], section='bindings'):
mathieui's avatar
mathieui committed
860