commands.py 47.6 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
from slixmpp.exceptions import XMPPError, IqError, IqTimeout
12
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
from poezio import common, config as config_module, tabs, multiuserchat as muc
18
from poezio.bookmarks import Bookmark
19
from poezio.config import config, DEFAULT_CONFIG
20
from poezio.contact import Contact, Resource
21
from poezio.decorators import deny_anonymous
22
23
24
25
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
26
from poezio.core.structs import Command, POSSIBLE_SHOW
mathieui's avatar
mathieui committed
27
28


29
30
31
log = logging.getLogger(__name__)


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

36
37
38
39
40
41
42
43
    @command_args_parser.ignored
    def rotate_rooms_left(self, args=None):
        self.core.rotate_rooms_left()

    @command_args_parser.ignored
    def rotate_rooms_right(self, args=None):
        self.core.rotate_rooms_right()

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

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

    @command_args_parser.quoted(1)
94
    def runkey(self, args):
95
96
97
        """
        /runkey <key>
        """
mathieui's avatar
mathieui committed
98

99
100
101
102
103
        def replace_line_breaks(key):
            "replace ^J with \n"
            if key == '^J':
                return '\n'
            return key
mathieui's avatar
mathieui committed
104

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

    @command_args_parser.quoted(1, 1, [None])
117
    def status(self, args):
118
119
120
121
        """
        /status <status> [msg]
        """
        if args is None:
122
            return self.help('status')
123

124
        if args[0] not in POSSIBLE_SHOW.keys():
125
            return self.help('status')
126
127
128
129

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

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

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

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

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

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

Jonas Schäfer's avatar
Jonas Schäfer committed
226
227
228
229
230
231
232
233
234
235
236
237
238
239
    @command_args_parser.quoted(1)
    def wup(self, args):
        """
        /wup <prefix of name>
        """
        if args is None:
            return self.help('wup')

        prefix = args[0]
        _, match = self.core.tabs.find_by_unique_prefix(prefix)
        if match is None:
            return
        self.core.tabs.set_current_tab(match)

240
    @command_args_parser.quoted(2)
241
    def move_tab(self, args):
242
243
244
245
        """
        /move_tab old_pos new_pos
        """
        if args is None:
246
            return self.help('move_tab')
247

248
        current_tab = self.core.tabs.current_tab
249
250
251
252
253
254
255
256
257
258
259
260
        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
261
                for tab in self.core.tabs:
262
263
264
                    if not old_tab and value == tab.name:
                        old_tab = tab
                if not old_tab:
mathieui's avatar
mathieui committed
265
266
                    self.core.information("Tab %s does not exist" % args[0],
                                          "Error")
267
268
269
                    return None
                ref = old_tab.nb
            return ref
mathieui's avatar
mathieui committed
270

271
272
273
        old = get_nb_from_value(args[0])
        new = get_nb_from_value(args[1])
        if new is None or old is None:
274
275
            return self.core.information('Unable to move the tab.', 'Info')
        result = self.core.insert_tab(old, new)
276
        if not result:
277
278
            self.core.information('Unable to move the tab.', 'Info')
        self.core.refresh_window()
279
280

    @command_args_parser.quoted(0, 1)
281
    def list(self, args: List[str]) -> None:
282
283
284
285
286
        """
        /list [server]
        Opens a MucListTab containing the list of the room in the specified server
        """
        if args is None:
287
            return self.help('list')
288
        elif args:
289
290
291
292
            try:
                jid = JID(args[0])
            except InvalidJID:
                return self.core.information('Invalid server %r' % jid, 'Error')
293
        else:
294
            if not isinstance(self.core.tabs.current_tab, tabs.MucTab):
mathieui's avatar
mathieui committed
295
296
                return self.core.information('Please provide a server',
                                             'Error')
297
298
299
            jid = self.core.tabs.current_tab.jid
        if jid is None or not jid.domain:
            return None
300
301
302
303
304
        asyncio.ensure_future(
            self._list_async(jid)
        )

    async def _list_async(self, jid: JID):
305
        jid = JID(jid.domain)
306
        list_tab = tabs.MucListTab(self.core, jid)
307
        self.core.add_tab(list_tab, True)
308
309
        iq = await self.core.xmpp.plugin['xep_0030'].get_items(jid=jid)
        list_tab.on_muc_list_item_received(iq)
310
311

    @command_args_parser.quoted(1)
312
    async def version(self, args):
313
314
315
316
        """
        /version <jid>
        """
        if args is None:
317
            return self.help('version')
318
319
320
321
322
323
324
        try:
            jid = JID(args[0])
        except InvalidJID:
            return self.core.information(
                'Invalid JID for /version: %s' % args[0],
                'Error'
            )
325
        if jid.resource or jid not in roster or not roster[jid].resources:
326
327
            iq = await self.core.xmpp.plugin['xep_0092'].get_version(jid)
            self.core.handler.on_version_result(iq)
328
329
        elif jid in roster:
            for resource in roster[jid].resources:
330
331
332
333
                iq = await self.core.xmpp.plugin['xep_0092'].get_version(
                    resource.jid
                )
                self.core.handler.on_version_result(iq)
334

mathieui's avatar
mathieui committed
335
    def _empty_join(self):
336
        tab = self.core.tabs.current_tab
mathieui's avatar
mathieui committed
337
338
        if not isinstance(tab, (tabs.MucTab, tabs.PrivateTab)):
            return (None, None)
339
        room = tab.jid.bare
mathieui's avatar
mathieui committed
340
341
342
        nick = tab.own_nick
        return (room, nick)

343
    def _parse_join_jid(self, jid_string: str) -> Tuple[Optional[str], Optional[str]]:
mathieui's avatar
mathieui committed
344
        # we try to join a server directly
mathieui's avatar
mathieui committed
345
        server_root = False
346
347
348
349
350
351
352
353
        try:
            if jid_string.startswith('@'):
                server_root = True
                info = JID(jid_string[1:])
            else:
                info = JID(jid_string)
                server_root = False
        except InvalidJID:
mathieui's avatar
mathieui committed
354
            info = JID('')
mathieui's avatar
mathieui committed
355

356
        set_nick: Optional[str] = ''
mathieui's avatar
mathieui committed
357
358
359
360
361
362
363
        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 == '':
364
            tab = self.core.tabs.current_tab
mathieui's avatar
mathieui committed
365
366
367
            if not isinstance(tab, tabs.MucTab):
                room, set_nick = (None, None)
            else:
368
                room = tab.jid.bare
mathieui's avatar
mathieui committed
369
370
371
372
373
374
375
376
                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:
377
                tab = self.core.tabs.current_tab
378
379
                if isinstance(tab, tabs.MucTab) and tab.jid.domain:
                    room += '@%s' % tab.jid.domain
mathieui's avatar
mathieui committed
380
381
        return (room, set_nick)

382
    @command_args_parser.quoted(0, 2)
mathieui's avatar
mathieui committed
383
    async def join(self, args):
384
385
386
387
        """
        /join [room][/nick] [password]
        """
        if len(args) == 0:
mathieui's avatar
mathieui committed
388
            room, nick = self._empty_join()
mathieui's avatar
mathieui committed
389
        else:
mathieui's avatar
mathieui committed
390
391
            room, nick = self._parse_join_jid(args[0])
        if not room and not nick:
mathieui's avatar
mathieui committed
392
            return  # nothing was parsed
mathieui's avatar
mathieui committed
393

394
        room = room.lower()
395
396
397

        # Has the nick been specified explicitely when joining
        config_nick = False
mathieui's avatar
mathieui committed
398
        if nick == '':
399
            config_nick = True
mathieui's avatar
mathieui committed
400
401
402
403
404
405
406
407
            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)

408
409
        if room in self.core.pending_invites:
            del self.core.pending_invites[room]
mathieui's avatar
mathieui committed
410

411
        tab = self.core.tabs.by_name_and_class(room, tabs.MucTab)
mathieui's avatar
mathieui committed
412
413
414
        # New tab
        if tab is None:
            tab = self.core.open_new_room(room, nick, password=password)
415
            tab.join()
mathieui's avatar
mathieui committed
416
        else:
417
            self.core.focus_tab(tab)
418
            if tab.own_nick == nick and tab.joined:
419
                self.core.information('/join: Nothing to do.', 'Info')
420
421
422
423
            else:
                tab.command_part('')
                tab.own_nick = nick
                tab.password = password
mathieui's avatar
mathieui committed
424
                tab.join()
mathieui's avatar
mathieui committed
425

426
427
        if config.getbool('synchronise_open_rooms') and room not in self.core.bookmarks:
            method = 'remote' if config.getbool(
mathieui's avatar
mathieui committed
428
                'use_remote_bookmarks') else 'local'
mathieui's avatar
mathieui committed
429
            await self._add_bookmark(
430
431
432
433
434
435
                room=room,
                nick=nick if not config_nick else None,
                autojoin=True,
                password=password,
                method=method,
            )
436

437
        if tab == self.core.tabs.current_tab:
mathieui's avatar
mathieui committed
438
439
            tab.refresh()
            self.core.doupdate()
440
441

    @command_args_parser.quoted(0, 2)
442
    def bookmark_local(self, args):
443
444
445
        """
        /bookmark_local [room][/nick] [password]
        """
446
447
        if not args and not isinstance(self.core.tabs.current_tab,
                                       tabs.MucTab):
448
            return
449
450

        room, nick = self._parse_join_jid(args[0] if args else '')
451
        password = args[1] if len(args) > 1 else None
mathieui's avatar
mathieui committed
452

mathieui's avatar
mathieui committed
453
454
455
456
457
458
459
460
        asyncio.ensure_future(
            self._add_bookmark(
                room=room,
                nick=nick,
                autojoin=True,
                password=password,
                method='local',
            )
461
        )
462
463

    @command_args_parser.quoted(0, 3)
464
    def bookmark(self, args):
465
466
467
        """
        /bookmark [room][/nick] [autojoin] [password]
        """
468
469
        if not args and not isinstance(self.core.tabs.current_tab,
                                       tabs.MucTab):
470
            return
471
        room, nick = self._parse_join_jid(args[0] if args else '')
472
473
        password = args[2] if len(args) > 2 else None

474
        method = 'remote' if config.getbool('use_remote_bookmarks') else 'local'
475
476
        autojoin = (method == 'local' or
                    (len(args) > 1 and args[1].lower() == 'true'))
477

mathieui's avatar
mathieui committed
478
479
480
        asyncio.ensure_future(
            self._add_bookmark(room, nick, autojoin, password, method)
        )
481

mathieui's avatar
mathieui committed
482
    async def _add_bookmark(
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
        self,
        room: Optional[str],
        nick: Optional[str],
        autojoin: bool,
        password: str,
        method: str,
    ) -> None:
        '''
        Adds a bookmark.

        Args:
            room: room Jid.
            nick: optional nick. Will always be added to the bookmark if
                specified. This takes precedence over tab.own_nick which takes
                precedence over core.own_nick (global config).
            autojoin: set the bookmark to join automatically.
            password: room password.
            method: 'local' or 'remote'.
        '''

        # No room Jid was specified. A nick may have been specified. Set the
        # room Jid to be bookmarked to the current tab bare jid.
        if not room:
506
            tab = self.core.tabs.current_tab
507
508
509
            if not isinstance(tab, tabs.MucTab):
                return
            room = tab.jid.bare
510
511
            if password is None and tab.password is not None:
                password = tab.password
512
        elif room == '*':
mathieui's avatar
mathieui committed
513
            return await self._add_wildcard_bookmarks(method)
514
515
516
517
518

        # Once we found which room to bookmark, find corresponding tab if it
        # exists and fill nickname if none was specified and not default.
        tab = self.core.tabs.by_name_and_class(room, tabs.MucTab)
        if tab and isinstance(tab, tabs.MucTab) and \
519
           tab.joined and tab.own_nick != self.core.own_nick:
520
521
522
523
524
525
526
527
528
529
530
531
532
533
            nick = nick or tab.own_nick

        # Validate / Normalize
        try:
            if nick is None:
                jid = JID(room)
            else:
                jid = JID('{}/{}'.format(room, nick))
            room = jid.bare
            nick = jid.resource or None
        except InvalidJID:
            return

        bookmark = self.core.bookmarks[room]
534
        if bookmark is None:
535
            bookmark = Bookmark(room)
536
            self.core.bookmarks.append(bookmark)
537
538
539
540
541
542
        bookmark.method = method
        bookmark.autojoin = autojoin
        if nick:
            bookmark.nick = nick
        if password:
            bookmark.password = password
mathieui's avatar
mathieui committed
543

544
        self.core.bookmarks.save_local()
mathieui's avatar
mathieui committed
545
546
547
548
549
550
551
        try:
            result = await self.core.bookmarks.save_remote(
                self.core.xmpp,
            )
            self.core.handler.on_bookmark_result(result)
        except (IqError, IqTimeout) as iq:
            self.core.handler.on_bookmark_result(iq)
552

mathieui's avatar
mathieui committed
553
    async def _add_wildcard_bookmarks(self, method):
554
        new_bookmarks = []
555
        for tab in self.core.get_tabs(tabs.MucTab):
556
            bookmark = self.core.bookmarks[tab.jid.bare]
557
            if not bookmark:
558
                bookmark = Bookmark(tab.jid.bare, autojoin=True, method=method)
559
560
561
562
                new_bookmarks.append(bookmark)
            else:
                bookmark.method = method
                new_bookmarks.append(bookmark)
563
564
565
566
                self.core.bookmarks.remove(bookmark)
        new_bookmarks.extend(self.core.bookmarks.bookmarks)
        self.core.bookmarks.set(new_bookmarks)
        self.core.bookmarks.save_local()
mathieui's avatar
mathieui committed
567
568
569
570
571
        try:
            iq = await self.core.bookmarks.save_remote(self.core.xmpp)
            self.core.handler.on_bookmark_result(iq)
        except IqError as iq:
            self.core.handler.on_bookmark_result(iq)
572
573

    @command_args_parser.ignored
574
    def bookmarks(self):
575
        """/bookmarks"""
576
577
        tab = self.core.tabs.by_name_and_class('Bookmarks', tabs.BookmarksTab)
        old_tab = self.core.tabs.current_tab
578
        if tab:
579
            self.core.tabs.set_current_tab(tab)
mathieui's avatar
mathieui committed
580
        else:
581
            tab = tabs.BookmarksTab(self.core, self.core.bookmarks)
582
            self.core.tabs.append(tab)
583
            self.core.tabs.set_current_tab(tab)
584
585

    @command_args_parser.quoted(0, 1)
586
    def remove_bookmark(self, args):
587
        """/remove_bookmark [jid]"""
mathieui's avatar
mathieui committed
588
589
590
591
592
593
594
595
596
597
598
        jid = None
        if not args:
            tab = self.core.tabs.current_tab
            if isinstance(tab, tabs.MucTab):
                jid = tab.jid.bare
        else:
            jid = args[0]

        asyncio.ensure_future(
            self._remove_bookmark_routine(jid)
        )
599

mathieui's avatar
mathieui committed
600
601
602
603
604
605
    async def _remove_bookmark_routine(self, jid: str):
        """Asynchronously remove a bookmark"""
        if self.core.bookmarks[jid]:
            self.core.bookmarks.remove(jid)
            try:
                await self.core.bookmarks.save(self.core.xmpp)
606
                self.core.information('Bookmark deleted', 'Info')
mathieui's avatar
mathieui committed
607
            except (IqError, IqTimeout):
mathieui's avatar
mathieui committed
608
609
                self.core.information('Error while deleting the bookmark',
                                      'Error')
mathieui's avatar
mathieui committed
610
        else:
mathieui's avatar
mathieui committed
611
            self.core.information('No bookmark to remove', 'Info')
612

613
    @deny_anonymous
614
    @command_args_parser.quoted(0, 1)
615
    def accept(self, args):
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
        """
        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:
631
632
633
634
635
636
            try:
                jid = JID(args[0]).bare
            except InvalidJID:
                return self.core.information('Invalid JID for /accept: %s' % args[0], 'Error')
        jid = JID(jid)
        nodepart = jid.user
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
        # 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')

653
    @deny_anonymous
654
    @command_args_parser.quoted(1)
655
    def add(self, args):
656
657
658
659
660
        """
        Add the specified JID to the roster, and automatically
        accept the reverse subscription
        """
        if args is None:
661
662
663
664
            tab = self.core.tabs.current_tab
            ConversationTab = tabs.ConversationTab
            if isinstance(tab, ConversationTab):
                jid = tab.general_jid
665
666
667
668
                if jid in roster and roster[jid].subscription in ('to', 'both'):
                    return self.core.information('Already subscribed.', 'Roster')
                roster.add(jid)
                roster.modified()
669
670
671
                return self.core.information('%s was added to the roster' % jid, 'Roster')
            else:
                return self.core.information('No JID specified', 'Error')
672
673
674
675
        try:
            jid = JID(args[0]).bare
        except InvalidJID:
            return self.core.information('Invalid JID for /add: %s' % args[0], 'Error')
676
677
678
679
680
        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')
681

682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
    @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:
697
698
699
700
            try:
                jid = JID(args[0]).bare
            except InvalidJID:
                return self.core.information('Invalid JID for /deny: %s' % args[0], 'Error')
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
            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:
722
723
724
725
            try:
                jid = JID(args[0]).bare
            except InvalidJID:
                return self.core.information('Invalid JID for /remove: %s' % args[0], 'Error')
726
727
728
729
730
731
732
733
734
735
736
737
        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]

738
739
740
741
742
743
744
745
    @command_args_parser.ignored
    def command_reconnect(self):
        """
        /reconnect
        """
        if self.core.xmpp.is_connected():
            self.core.disconnect(reconnect=True)
        else:
746
            self.core.xmpp.start()
747

748
    @command_args_parser.quoted(0, 3)
749
    def set(self, args):
750
751
752
        """
        /set [module|][section] <option> [value]
        """
753
754
        if len(args) == 3 and args[1] == '=':
            args = [args[0], args[2]]
755
756
757
758
759
        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
760
761
762
763
764
765
                lines.append(
                    '\x19%(section_col)s}[%(section)s]\x19o' % {
                        'section': section_name,
                        'section_col': dump_tuple(
                            theme.COLOR_INFORMATION_TEXT),
                    })
766
                for option_name, option_value in section.items():
767
768
                    if isinstance(option_name, str) and \
                        'password' in option_name and 'eval_password' not in option_name:
769
                        option_value = '********'
mathieui's avatar
mathieui committed
770
771
772
773
                    lines.append(
                        '%s\x19%s}=\x19o%s' %
                        (option_name, dump_tuple(
                            theme.COLOR_REVISIONS_MESSAGE), option_value))
774
775
776
777
            info = ('Current  options:\n%s' % '\n'.join(lines), 'Info')
        elif len(args) == 1:
            option = args[0]
            value = config.get(option)
778
779
            if isinstance(option, str) and \
                'password' in option and 'eval_password' not in option and value is not None:
780
                value = '********'
781
782
            if value is None and '=' in option:
                args = option.split('=', 1)
783
            info = ('%s=%s' % (option, value), 'Info')
784
785
786
787
788
        if len(args) == 2:
            if '|' in args[0]:
                plugin_name, section = args[0].split('|')[:2]
                if not section:
                    section = plugin_name
789
                option = args[1]
790
                if plugin_name not in self.core.plugin_manager.plugins:
mathieui's avatar
mathieui committed
791
792
                    file_name = self.core.plugin_manager.plugins_conf_dir / (
                        plugin_name + '.cfg')
793
794
                    plugin_config = PluginConfig(file_name, plugin_name)
                else:
mathieui's avatar
mathieui committed
795
796
                    plugin_config = self.core.plugin_manager.plugins[
                        plugin_name].config
797
                value = plugin_config.get(option, default='', section=section)
798
799
                info = ('%s=%s' % (option, value), 'Info')
            else:
800
801
802
803
804
805
806
807
808
809
                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)
810
                    self.core.trigger_configuration_change(option, value)
811
812
813
814
815
816
817
        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]
818
                if plugin_name not in self.core.plugin_manager.plugins:
mathieui's avatar
mathieui committed
819
820
                    file_name = self.core.plugin_manager.plugins_conf_dir / (
                        plugin_name + '.cfg')
821
822
                    plugin_config = PluginConfig(file_name, plugin_name)
                else:
mathieui's avatar
mathieui committed
823
824
                    plugin_config = self.core.plugin_manager.plugins[
                        plugin_name].config
825
826
827
                info = plugin_config.set_and_save(option, value, section)
            else:
                if args[0] == '.':
828
                    name = self.core.tabs.current_tab.jid.bare
829
                    if not name:
mathieui's avatar
mathieui committed
830
831
                        self.core.information(
                            'Invalid tab to use the "." argument.', 'Error')
832
833
834
835
836
837
                        return
                    section = name
                else:
                    section = args[0]
                option = args[1]
                value = args[2]
Madhur Garg's avatar
Madhur Garg committed
838
                info = config.set_and_save(option, value, section)
839
                self.core.trigger_configuration_change(option, value)
840
        elif len(args) > 3:
841
842
            return self.help('set')
        self.core.information(*info)
843
844

    @command_args_parser.quoted(1, 2)
845
    def set_default(self, args):
846
847
848
849
850
851
852
853
        """
        /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
854
855
            option = args[1]
        else:
856
            return self.help('set_default')
857
858
859
860

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

    @command_args_parser.quoted(1)
865
    def toggle(self, args):
866
867
868
869
870
        """
        /toggle <option>
        shortcut for /set <option> toggle
        """
        if args is None:
871
            return self.help('toggle')
872
873

        if args[0]:
874
            self.set('%s toggle' % args[0])
875
876

    @command_args_parser.quoted(1, 1)
877
    def server_cycle(self, args):
878
879
        """
        Do a /cycle on each room of the given server.
880
        If none, do it on the server of the current tab
881
        """
882
        tab = self.core.tabs.current_tab
883
884
        message = ""
        if args:
885
886
887
888
889
890
891
            try:
                domain = JID(args[0]).domain
            except InvalidJID:
                return self.core.information(
                    "Invalid server domain: %s" % args[0],
                    "Error"
                )
892
893
            if len(args) == 2:
                message = args[1]
mathieui's avatar
mathieui committed
894
        else:
895
            if isinstance(tab, tabs.MucTab):
896
                domain = tab.jid.domain
mathieui's avatar
mathieui committed
897
            else:
898
899
                return self.core.information("No server specified", "Error")
        for tab in self.core.get_tabs(tabs.MucTab):
900
            if tab.jid.domain == domain:
mathieui's avatar
mathieui committed
901
902
                tab.leave_room(message)
                tab.join()
903
904

    @command_args_parser.quoted(1)
905
    async def last_activity(self, args):
906
907
908
        """
        /last_activity <jid>
        """
mathieui's avatar
mathieui committed
909

910
        if args is None:
911
            return self.help('last_activity')
912
913
914
915
        try:
            jid = JID(args[0])
        except InvalidJID:
            return self.core.information('Invalid JID for /last_activity: %s' % args[0], 'Error')
916
917

        try:
mathieui's avatar
mathieui committed
918
            iq = await self.core.xmpp.plugin['xep_0012'].get_last_activity(jid)
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
        except IqError as error:
            if error.etype == 'auth':
                msg = 'You are not allowed to see the activity of %s' % jid
            else:
                msg = 'Error retrieving the activity of %s: %s' % (jid, error)
            return self.core.information(msg, 'Error')
        except IqTimeout:
            return self.core.information('Timeout while retrieving the last activity of %s' % jid, 'Error')

        seconds = iq['last_activity']['seconds']
        status = iq['last_activity']['status']
        from_ = iq['from']
        if not from_.user:
            msg = 'The uptime of %s is %s.' % (
                from_, common.parse_secs_to_str(seconds))
        else:
            msg = 'The last activity of %s was %s ago%s' % (
                from_, common.parse_secs_to_str(seconds),
                (' and their last status was %s' % status)
                if status else '')
        self.core.information(msg, 'Info')
940
941

    @command_args_parser.quoted(2, 1, [None])
942
    async def invite(self, args):
943
944
945
        """/invite <to> <room> [reason]"""

        if args is None:
946
            return self.help('invite')
947
948

        reason = args[2]
949
950
951
952
953
954
955
956
957
958
        try:
            to = JID(args[0])
        except InvalidJID:
            self.core.information('Invalid JID specified for invite: %s' % args[0], 'Error')
            return None
        try:
            room = JID(args[1]).bare
        except InvalidJID:
            self.core.information('Invalid room JID specified to invite: %s' % args[1], 'Error')
            return None
959
960
961
        result = await self.core.invite(to.full, room, reason=reason)
        if result:
            self.core.information('Invited %s to %s' % (to.bare, room), 'Info')
962

Maxime Buquet's avatar
Maxime Buquet committed
963
    @command_args_parser.quoted(1, 0)
964
    def impromptu(self, args: str) -> None:
Maxime Buquet's avatar
Maxime Buquet committed
965
966
967
968
969
        """/impromptu <jid> [<jid> ...]"""

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

970
971
972
973
974
        jids = set()
        current_tab = self.core