diff options
Diffstat (limited to 'poezio/core/commands.py')
-rw-r--r-- | poezio/core/commands.py | 857 |
1 files changed, 616 insertions, 241 deletions
diff --git a/poezio/core/commands.py b/poezio/core/commands.py index 86df9a93..fe91ca67 100644 --- a/poezio/core/commands.py +++ b/poezio/core/commands.py @@ -2,38 +2,46 @@ Global commands which are to be linked to the Core class """ -import logging - -log = logging.getLogger(__name__) - import asyncio -from xml.etree import cElementTree as ET +from urllib.parse import unquote +from xml.etree import ElementTree as ET +from typing import List, Optional, Tuple +import logging -from slixmpp.exceptions import XMPPError +from slixmpp import JID, InvalidJID +from slixmpp.exceptions import XMPPError, IqError, IqTimeout from slixmpp.xmlstream.xmlstream import NotConnectedError from slixmpp.xmlstream.stanzabase import StanzaBase from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.matcher import StanzaPath -from poezio import common -from poezio import pep -from poezio import tabs +from poezio import common, config as config_module, tabs, multiuserchat as muc 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.config import config, DEFAULT_CONFIG +from poezio.contact import Contact, Resource +from poezio.decorators import deny_anonymous 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 - from poezio.core.structs import Command, POSSIBLE_SHOW +log = logging.getLogger(__name__) + + class CommandCore: def __init__(self, core): self.core = core + @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() + @command_args_parser.quoted(0, 1) def help(self, args): """ @@ -132,7 +140,7 @@ class CommandCore: current.send_chat_state('inactive') for tab in self.core.tabs: if isinstance(tab, tabs.MucTab) and tab.joined: - muc.change_show(self.core.xmpp, tab.name, tab.own_nick, show, + muc.change_show(self.core.xmpp, tab.jid, tab.own_nick, show, msg) if hasattr(tab, 'directed_presence'): del tab.directed_presence @@ -150,7 +158,7 @@ class CommandCore: jid, ptype, status = args[0], args[1], args[2] if jid == '.' and isinstance(self.core.tabs.current_tab, tabs.ChatTab): - jid = self.core.tabs.current_tab.name + jid = self.core.tabs.current_tab.jid if ptype == 'available': ptype = None try: @@ -216,6 +224,20 @@ class CommandCore: return self.core.tabs.set_current_tab(match) + @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) + @command_args_parser.quoted(2) def move_tab(self, args): """ @@ -257,7 +279,7 @@ class CommandCore: self.core.refresh_window() @command_args_parser.quoted(0, 1) - def list(self, args): + def list(self, args: List[str]) -> None: """ /list [server] Opens a MucListTab containing the list of the room in the specified server @@ -265,51 +287,76 @@ class CommandCore: if args is None: return self.help('list') elif args: - jid = safeJID(args[0]) + try: + jid = JID(args[0]) + except InvalidJID: + return self.core.information('Invalid server %r' % jid, 'Error') else: if not isinstance(self.core.tabs.current_tab, tabs.MucTab): return self.core.information('Please provide a server', 'Error') - jid = safeJID(self.core.tabs.current_tab.name) + jid = self.core.tabs.current_tab.jid + if jid is None or not jid.domain: + return None + asyncio.create_task( + self._list_async(jid) + ) + + async def _list_async(self, jid: JID): + jid = JID(jid.domain) list_tab = tabs.MucListTab(self.core, jid) self.core.add_tab(list_tab, True) - cb = list_tab.on_muc_list_item_received - self.core.xmpp.plugin['xep_0030'].get_items(jid=jid, callback=cb) + iq = await self.core.xmpp.plugin['xep_0030'].get_items(jid=jid) + list_tab.on_muc_list_item_received(iq) @command_args_parser.quoted(1) - def version(self, args): + async def version(self, args): """ /version <jid> """ if args is None: return self.help('version') - jid = safeJID(args[0]) + try: + jid = JID(args[0]) + except InvalidJID: + return self.core.information( + 'Invalid JID for /version: %s' % args[0], + 'Error' + ) if jid.resource or jid not in roster or not roster[jid].resources: - self.core.xmpp.plugin['xep_0092'].get_version( - jid, callback=self.core.handler.on_version_result) + iq = await self.core.xmpp.plugin['xep_0092'].get_version(jid) + self.core.handler.on_version_result(iq) elif jid in roster: for resource in roster[jid].resources: - self.core.xmpp.plugin['xep_0092'].get_version( - resource.jid, callback=self.core.handler.on_version_result) + iq = await self.core.xmpp.plugin['xep_0092'].get_version( + resource.jid + ) + self.core.handler.on_version_result(iq) def _empty_join(self): tab = self.core.tabs.current_tab if not isinstance(tab, (tabs.MucTab, tabs.PrivateTab)): return (None, None) - room = safeJID(tab.name).bare + room = tab.jid.bare nick = tab.own_nick return (room, nick) - def _parse_join_jid(self, jid_string): + def _parse_join_jid(self, jid_string: str) -> Tuple[Optional[str], Optional[str]]: # 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 + server_root = False + if jid_string.startswith('xmpp:') and jid_string.endswith('?join'): + jid_string = unquote(jid_string[5:-5]) + try: + if jid_string.startswith('@'): + server_root = True + info = JID(jid_string[1:]) + else: + info = JID(jid_string) + server_root = False + except InvalidJID: + info = JID('') - set_nick = '' + set_nick: Optional[str] = '' if len(jid_string) > 1 and jid_string.startswith('/'): set_nick = jid_string[1:] elif info.resource: @@ -321,7 +368,7 @@ class CommandCore: if not isinstance(tab, tabs.MucTab): room, set_nick = (None, None) else: - room = tab.name + room = tab.jid.bare if not set_nick: set_nick = tab.own_nick else: @@ -331,14 +378,12 @@ class CommandCore: # check if the current room's name has a server if room.find('@') == -1 and not server_root: tab = self.core.tabs.current_tab - if isinstance(tab, tabs.MucTab): - if tab.name.find('@') != -1: - domain = safeJID(tab.name).domain - room += '@%s' % domain + if isinstance(tab, tabs.MucTab) and tab.jid.domain: + room += '@%s' % tab.jid.domain return (room, set_nick) @command_args_parser.quoted(0, 2) - def join(self, args): + async def join(self, args): """ /join [room][/nick] [password] """ @@ -350,7 +395,11 @@ class CommandCore: return # nothing was parsed room = room.lower() + + # Has the nick been specified explicitely when joining + config_nick = False if nick == '': + config_nick = True nick = self.core.own_nick # a password is provided @@ -377,10 +426,16 @@ class CommandCore: tab.password = password tab.join() - if config.get('bookmark_on_join'): - method = 'remote' if config.get( + if config.getbool('synchronise_open_rooms') and room not in self.core.bookmarks: + method = 'remote' if config.getbool( 'use_remote_bookmarks') else 'local' - self._add_bookmark('%s/%s' % (room, nick), True, password, method) + await self._add_bookmark( + room=room, + nick=nick if not config_nick else None, + autojoin=True, + password=password, + method=method, + ) if tab == self.core.tabs.current_tab: tab.refresh() @@ -391,57 +446,99 @@ class CommandCore: """ /bookmark_local [room][/nick] [password] """ - if not args and not isinstance(self.core.tabs.current_tab, - tabs.MucTab): + tab = self.core.tabs.current_tab + if not args and not isinstance(tab, tabs.MucTab): return + + room, nick = self._parse_join_jid(args[0] if args else '') password = args[1] if len(args) > 1 else None - jid = args[0] if args else None - self._add_bookmark(jid, True, password, 'local') + if not room: + room = tab.jid.bare + if password is None and tab.password is not None: + password = tab.password + + asyncio.create_task( + self._add_bookmark( + room=room, + nick=nick, + autojoin=True, + password=password, + method='local', + ) + ) @command_args_parser.quoted(0, 3) def bookmark(self, args): """ /bookmark [room][/nick] [autojoin] [password] """ - if not args and not isinstance(self.core.tabs.current_tab, - tabs.MucTab): + tab = self.core.tabs.current_tab + if not args and not isinstance(tab, tabs.MucTab): return - jid = args[0] if args else '' + room, nick = self._parse_join_jid(args[0] if args else '') password = args[2] if len(args) > 2 else None - if not config.get('use_remote_bookmarks'): - return self._add_bookmark(jid, True, password, 'local') - - if len(args) > 1: - autojoin = False if args[1].lower() != 'true' else True - else: - autojoin = True + method = 'remote' if config.getbool('use_remote_bookmarks') else 'local' + autojoin = (method == 'local' or + (len(args) > 1 and args[1].lower() == 'true')) + + if not room: + room = tab.jid.bare + if password is None and tab.password is not None: + password = tab.password + + asyncio.create_task( + self._add_bookmark(room, nick, autojoin, password, method) + ) + + async def _add_bookmark( + self, + room: 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'. + ''' + + + if room == '*': + return await self._add_wildcard_bookmarks(method) + + # 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 \ + tab.joined and tab.own_nick != self.core.own_nick: + nick = nick or tab.own_nick - self._add_bookmark(jid, autojoin, password, 'remote') + # Validate / Normalize + try: + if not nick: + jid = JID(room) + else: + jid = JID('{}/{}'.format(room, nick)) + room = jid.bare + nick = jid.resource or None + except InvalidJID: + self.core.information(f'Invalid address for bookmark: {room}/{nick}', 'Error') + return - def _add_bookmark(self, jid, autojoin, password, method): - nick = None - if not jid: - tab = self.core.tabs.current_tab - roomname = tab.name - if tab.joined and tab.own_nick != self.core.own_nick: - nick = tab.own_nick - if password is None and tab.password is not None: - password = tab.password - elif jid == '*': - return self._add_wildcard_bookmarks(method) - else: - info = safeJID(jid) - roomname, nick = info.bare, info.resource - if roomname == '': - tab = self.core.tabs.current_tab - if not isinstance(tab, tabs.MucTab): - return - roomname = tab.name - bookmark = self.core.bookmarks[roomname] + bookmark = self.core.bookmarks[room] if bookmark is None: - bookmark = Bookmark(roomname) + bookmark = Bookmark(room) self.core.bookmarks.append(bookmark) bookmark.method = method bookmark.autojoin = autojoin @@ -451,15 +548,20 @@ class CommandCore: bookmark.password = password self.core.bookmarks.save_local() - self.core.bookmarks.save_remote(self.core.xmpp, - self.core.handler.on_bookmark_result) - - def _add_wildcard_bookmarks(self, method): + 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) + + async def _add_wildcard_bookmarks(self, method): new_bookmarks = [] for tab in self.core.get_tabs(tabs.MucTab): - bookmark = self.core.bookmarks[tab.name] + bookmark = self.core.bookmarks[tab.jid.bare] if not bookmark: - bookmark = Bookmark(tab.name, autojoin=True, method=method) + bookmark = Bookmark(tab.jid.bare, autojoin=True, method=method) new_bookmarks.append(bookmark) else: bookmark.method = method @@ -468,8 +570,11 @@ class CommandCore: new_bookmarks.extend(self.core.bookmarks.bookmarks) self.core.bookmarks.set(new_bookmarks) self.core.bookmarks.save_local() - self.core.bookmarks.save_remote(self.core.xmpp, - self.core.handler.on_bookmark_result) + 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) @command_args_parser.ignored def bookmarks(self): @@ -486,33 +591,173 @@ class CommandCore: @command_args_parser.quoted(0, 1) def remove_bookmark(self, args): """/remove_bookmark [jid]""" + 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.create_task( + self._remove_bookmark_routine(jid) + ) - def cb(success): - if success: + 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) self.core.information('Bookmark deleted', 'Info') - else: + except (IqError, IqTimeout): self.core.information('Error while deleting the bookmark', 'Error') + else: + self.core.information('No bookmark to remove', 'Info') + @deny_anonymous + @command_args_parser.quoted(0, 1) + def accept(self, args): + """ + Accept a JID. Authorize it AND subscribe to it + """ if not args: tab = self.core.tabs.current_tab - 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) + RosterInfoTab = tabs.RosterInfoTab + if not isinstance(tab, RosterInfoTab): + return self.core.information('No JID specified', 'Error') else: - self.core.information('No bookmark to remove', 'Info') + item = tab.selected_row + if isinstance(item, Contact): + jid = item.bare_jid + else: + return self.core.information('No subscription to accept', 'Warning') else: - if self.core.bookmarks[args[0]]: - self.core.bookmarks.remove(args[0]) - self.core.bookmarks.save(self.core.xmpp, callback=cb) + 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 + # 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') + + @deny_anonymous + @command_args_parser.quoted(1) + def add(self, args): + """ + Add the specified JID to the roster, and automatically + accept the reverse subscription + """ + if args is None: + tab = self.core.tabs.current_tab + ConversationTab = tabs.ConversationTab + if isinstance(tab, ConversationTab): + jid = tab.general_jid + if jid in roster and roster[jid].subscription in ('to', 'both'): + return self.core.information('Already subscribed.', 'Roster') + roster.add(jid) + roster.modified() + return self.core.information('%s was added to the roster' % jid, 'Roster') else: - self.core.information('No bookmark to remove', 'Info') + return self.core.information('No JID specified', 'Error') + try: + jid = JID(args[0]).bare + except InvalidJID: + return self.core.information('Invalid JID for /add: %s' % args[0], 'Error') + 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') + + @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: + try: + jid = JID(args[0]).bare + except InvalidJID: + return self.core.information('Invalid JID for /deny: %s' % args[0], 'Error') + 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: + try: + jid = JID(args[0]).bare + except InvalidJID: + return self.core.information('Invalid JID for /remove: %s' % args[0], 'Error') + 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] + + @command_args_parser.ignored + def command_reconnect(self): + """ + /reconnect + """ + if self.core.xmpp.is_connected(): + self.core.disconnect(reconnect=True) + else: + self.core.xmpp.start() @command_args_parser.quoted(0, 3) def set(self, args): """ /set [module|][section] <option> [value] """ + if len(args) == 3 and args[1] == '=': + args = [args[0], args[2]] if args is None or len(args) == 0: config_dict = config.to_dict() lines = [] @@ -525,6 +770,9 @@ class CommandCore: theme.COLOR_INFORMATION_TEXT), }) for option_name, option_value in section.items(): + if isinstance(option_name, str) and \ + 'password' in option_name and 'eval_password' not in option_name: + option_value = '********' lines.append( '%s\x19%s}=\x19o%s' % (option_name, dump_tuple( @@ -533,6 +781,9 @@ class CommandCore: elif len(args) == 1: option = args[0] value = config.get(option) + if isinstance(option, str) and \ + 'password' in option and 'eval_password' not in option and value is not None: + value = '********' if value is None and '=' in option: args = option.split('=', 1) info = ('%s=%s' % (option, value), 'Info') @@ -553,7 +804,8 @@ class CommandCore: info = ('%s=%s' % (option, value), 'Info') else: possible_section = args[0] - if config.has_section(possible_section): + if (not config.has_option(section='Poezio', option=possible_section) + and config.has_section(possible_section)): section = possible_section option = args[1] value = config.get(option, section=section) @@ -580,7 +832,7 @@ class CommandCore: info = plugin_config.set_and_save(option, value, section) else: if args[0] == '.': - name = safeJID(self.core.tabs.current_tab.name).bare + name = self.core.tabs.current_tab.jid.bare if not name: self.core.information( 'Invalid tab to use the "." argument.', 'Error') @@ -632,137 +884,88 @@ class CommandCore: def server_cycle(self, args): """ Do a /cycle on each room of the given server. - If none, do it on the current tab + If none, do it on the server of the current tab """ tab = self.core.tabs.current_tab message = "" if args: - domain = args[0] + try: + domain = JID(args[0]).domain + except InvalidJID: + return self.core.information( + "Invalid server domain: %s" % args[0], + "Error" + ) if len(args) == 2: message = args[1] else: if isinstance(tab, tabs.MucTab): - domain = safeJID(tab.name).domain + domain = tab.jid.domain else: return self.core.information("No server specified", "Error") for tab in self.core.get_tabs(tabs.MucTab): - if tab.name.endswith(domain): + if tab.jid.domain == domain: tab.leave_room(message) tab.join() @command_args_parser.quoted(1) - def last_activity(self, args): + async def last_activity(self, args): """ /last_activity <jid> """ - def callback(iq): - "Callback for the last activity" - if iq['type'] != 'result': - if iq['error']['type'] == 'auth': - self.core.information( - 'You are not allowed to see the ' - 'activity of this contact.', 'Error') - else: - self.core.information('Error retrieving the activity', - 'Error') - 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.' % ( - 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 his/her last status was %s' % status) - if status else '') - self.core.information(msg, 'Info') - if args is None: return self.help('last_activity') - jid = safeJID(args[0]) - self.core.xmpp.plugin['xep_0012'].get_last_activity( - jid, callback=callback) - - @command_args_parser.quoted(0, 2) - def mood(self, args): - """ - /mood [<mood> [text]] - """ - if not args: - return self.core.xmpp.plugin['xep_0107'].stop() - - mood = args[0] - if mood not in pep.MOODS: - return self.core.information( - '%s is not a correct value for a mood.' % mood, 'Error') - if len(args) == 2: - text = args[1] - else: - text = None - self.core.xmpp.plugin['xep_0107'].publish_mood( - mood, text, callback=dumb_callback) - - @command_args_parser.quoted(0, 3) - def activity(self, args): - """ - /activity [<general> [specific] [text]] - """ - length = len(args) - if not length: - return self.core.xmpp.plugin['xep_0108'].stop() + try: + jid = JID(args[0]) + except InvalidJID: + return self.core.information('Invalid JID for /last_activity: %s' % args[0], 'Error') - general = args[0] - if general not in pep.ACTIVITIES: - return self.core.information( - '%s is not a correct value for an activity' % general, 'Error') - specific = None - text = None - if length == 2: - if args[1] in pep.ACTIVITIES[general]: - specific = args[1] + try: + iq = await self.core.xmpp.plugin['xep_0012'].get_last_activity(jid) + except IqError as error: + if error.etype == 'auth': + msg = 'You are not allowed to see the activity of %s' % jid else: - text = args[1] - elif length == 3: - specific = args[1] - text = args[2] - if specific and specific not in pep.ACTIVITIES[general]: - return self.core.information( - '%s is not a correct value ' - 'for an activity' % specific, 'Error') - self.core.xmpp.plugin['xep_0108'].publish_activity( - general, specific, text, callback=dumb_callback) - - @command_args_parser.quoted(0, 2) - def gaming(self, args): - """ - /gaming [<game name> [server address]] - """ - if not args: - return self.core.xmpp.plugin['xep_0196'].stop() - - name = args[0] - if len(args) > 1: - address = args[1] + 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: - address = None - return self.core.xmpp.plugin['xep_0196'].publish_gaming( - name=name, server_address=address, callback=dumb_callback) + 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') @command_args_parser.quoted(2, 1, [None]) - def invite(self, args): + async def invite(self, args): """/invite <to> <room> [reason]""" if args is None: return self.help('invite') reason = args[2] - to = safeJID(args[0]) - room = safeJID(args[1]).bare - self.core.invite(to.full, room, reason=reason) - self.core.information('Invited %s to %s' % (to.bare, room), 'Info') + 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 + result = await self.core.invite(to.full, room, reason=reason) + if result: + self.core.information('Invited %s to %s' % (to.bare, room), 'Info') @command_args_parser.quoted(1, 0) def impromptu(self, args: str) -> None: @@ -777,17 +980,23 @@ class CommandCore: jids.add(current_tab.general_jid) for jid in common.shell_split(' '.join(args)): - jids.add(safeJID(jid).bare) + try: + bare = JID(jid).bare + except InvalidJID: + return self.core.information('Invalid JID for /impromptu: %s' % args[0], 'Error') + jids.add(JID(bare)) - asyncio.ensure_future(self.core.impromptu(jids)) - self.core.information('Invited %s to a random room' % (' '.join(jids)), 'Info') + asyncio.create_task(self.core.impromptu(jids)) @command_args_parser.quoted(1, 1, ['']) def decline(self, args): """/decline <room@server.tld> [reason]""" if args is None: return self.help('decline') - jid = safeJID(args[0]) + try: + jid = JID(args[0]) + except InvalidJID: + return self.core.information('Invalid JID for /decline: %s' % args[0], 'Error') if jid.bare not in self.core.pending_invites: return reason = args[1] @@ -795,21 +1004,135 @@ class CommandCore: self.core.xmpp.plugin['xep_0045'].decline_invite( jid.bare, self.core.pending_invites[jid.bare], reason) + @command_args_parser.quoted(0, 1) + def block(self, args: List[str]) -> None: + """ + /block [jid] + + If a JID is specified, use it. Otherwise if in RosterInfoTab, use the + selected JID, if in ConversationsTab use the Tab's JID. + """ + + jid = None + if args: + try: + jid = JID(args[0]) + except InvalidJID: + self.core.information('Invalid JID %s' % args, 'Error') + return + + current_tab = self.core.tabs.current_tab + if jid is None: + if isinstance(current_tab, tabs.RosterInfoTab): + roster_win = self.core.tabs.by_name_and_class( + 'Roster', + tabs.RosterInfoTab, + ) + item = roster_win.selected_row + if isinstance(item, Contact): + jid = item.bare_jid + elif isinstance(item, Resource): + jid = JID(item.jid) + + chattabs = ( + tabs.ConversationTab, + tabs.StaticConversationTab, + tabs.DynamicConversationTab, + ) + if isinstance(current_tab, chattabs): + jid = JID(current_tab.jid.bare) + + if jid is None: + self.core.information('No specified JID to block', 'Error') + else: + asyncio.create_task(self._block_async(jid)) + + async def _block_async(self, jid: JID): + """Block a JID, asynchronously""" + try: + await self.core.xmpp.plugin['xep_0191'].block(jid) + return self.core.information('Blocked %s.' % jid, 'Info') + except (IqError, IqTimeout): + return self.core.information( + 'Could not block %s.' % jid, 'Error', + ) + + @command_args_parser.quoted(0, 1) + def unblock(self, args: List[str]) -> None: + """ + /unblock [jid] + """ + + item = self.core.tabs.by_name_and_class( + 'Roster', + tabs.RosterInfoTab, + ).selected_row + + jid = None + if args: + try: + jid = JID(args[0]) + except InvalidJID: + self.core.information('Invalid JID %s' % args, 'Error') + return + + current_tab = self.core.tabs.current_tab + if jid is None: + if isinstance(current_tab, tabs.RosterInfoTab): + roster_win = self.core.tabs.by_name_and_class( + 'Roster', + tabs.RosterInfoTab, + ) + item = roster_win.selected_row + if isinstance(item, Contact): + jid = item.bare_jid + elif isinstance(item, Resource): + jid = JID(item.jid) + + chattabs = ( + tabs.ConversationTab, + tabs.StaticConversationTab, + tabs.DynamicConversationTab, + ) + if isinstance(current_tab, chattabs): + jid = JID(current_tab.jid.bare) + + if jid is not None: + asyncio.create_task( + self._unblock_async(jid) + ) + else: + self.core.information('No specified JID to unblock', 'Error') + + async def _unblock_async(self, jid: JID): + """Unblock a JID, asynchrously""" + try: + await self.core.xmpp.plugin['xep_0191'].unblock(jid) + return self.core.information('Unblocked %s.' % jid, 'Info') + except (IqError, IqTimeout): + return self.core.information('Could not unblock the contact.', + 'Error') ### Commands without a completion in this class ### @command_args_parser.ignored def invitations(self): """/invitations""" - build = "" - for invite in self.core.pending_invites: - build += "%s by %s" % ( - invite, safeJID(self.core.pending_invites[invite]).bare) - if self.core.pending_invites: - build = "You are invited to the following rooms:\n" + build + build = [] + for room, inviter in self.core.pending_invites.items(): + try: + bare = JID(inviter).bare + except InvalidJID: + self.core.information( + f'Invalid JID found in /invitations: {inviter}', + 'Error' + ) + build.append(f'{room} by {bare}') + if build: + message = 'You are invited to the following rooms:\n' + ','.join(build) else: - build = "You do not have any pending invitations." - self.core.information(build, 'Info') + message = 'You do not have any pending invitations.' + self.core.information(message, 'Info') @command_args_parser.quoted(0, 1, [None]) def quit(self, args): @@ -821,32 +1144,51 @@ class CommandCore: return msg = args[0] - if config.get('enable_user_mood'): - self.core.xmpp.plugin['xep_0107'].stop() - if config.get('enable_user_activity'): - self.core.xmpp.plugin['xep_0108'].stop() - if config.get('enable_user_gaming'): - self.core.xmpp.plugin['xep_0196'].stop() self.core.save_config() self.core.plugin_manager.disable_plugins() - self.core.disconnect(msg) self.core.xmpp.add_event_handler( "disconnected", self.core.exit, disposable=True) + self.core.disconnect(msg) - @command_args_parser.quoted(0, 1, ['']) - def destroy_room(self, args): + @command_args_parser.quoted(0, 3, ['', '', '']) + def destroy_room(self, args: List[str]): """ - /destroy_room [JID] + /destroy_room [JID [reason [alternative room JID]]] """ - room = safeJID(args[0]).bare - if room: - muc.destroy_room(self.core.xmpp, room) - elif isinstance(self.core.tabs.current_tab, - tabs.MucTab) and not args[0]: - muc.destroy_room(self.core.xmpp, - self.core.tabs.current_tab.general_jid) + async def do_destroy(room: JID, reason: str, altroom: Optional[JID]): + try: + await self.core.xmpp['xep_0045'].destroy(room, reason, altroom) + except (IqError, IqTimeout) as e: + self.core.information('Unable to destroy room %s: %s' % (room, e), 'Info') + else: + self.core.information('Room %s destroyed' % room, 'Info') + + room: Optional[JID] + if not args[0] and isinstance(self.core.tabs.current_tab, tabs.MucTab): + room = self.core.tabs.current_tab.general_jid else: - self.core.information('Invalid JID: "%s"' % args[0], 'Error') + try: + room = JID(args[0]) + except InvalidJID: + room = None + else: + if room.resource: + room = None + + if room is None: + self.core.information('Invalid room JID: "%s"' % args[0], 'Error') + return + + reason = args[1] + altroom = None + if args[2]: + try: + altroom = JID(args[2]) + except InvalidJID: + self.core.information('Invalid alternative room JID: "%s"' % args[2], 'Error') + return + + asyncio.create_task(do_destroy(room, reason, altroom)) @command_args_parser.quoted(1, 1, ['']) def bind(self, args): @@ -903,11 +1245,17 @@ class CommandCore: exc_info=True) @command_args_parser.quoted(1, 256) - def load(self, args): + def load(self, args: List[str]) -> None: """ /load <plugin> [<otherplugin> …] # TODO: being able to load more than 256 plugins at once, hihi. """ + + usage = '/load <plugin> [<otherplugin> …]' + if not args: + self.core.information(usage, 'Error') + return + for plugin in args: self.core.plugin_manager.load(plugin) @@ -916,6 +1264,12 @@ class CommandCore: """ /unload <plugin> [<otherplugin> …] """ + + usage = '/unload <plugin> [<otherplugin> …]' + if not args: + self.core.information(usage, 'Error') + return + for plugin in args: self.core.plugin_manager.unload(plugin) @@ -929,20 +1283,23 @@ class CommandCore: list(self.core.plugin_manager.plugins.keys())), 'Info') @command_args_parser.quoted(1, 1) - def message(self, args): + async def message(self, args): """ /message <jid> [message] """ if args is None: return self.help('message') - jid = safeJID(args[0]) + try: + jid = JID(args[0]) + except InvalidJID: + return self.core.information('Invalid JID for /message: %s' % args[0], 'Error') if not jid.user and not jid.domain and not jid.resource: return self.core.information('Invalid JID.', 'Error') tab = self.core.get_conversation_by_jid( jid.full, False, fallback_barejid=False) muc = self.core.tabs.by_name_and_class(jid.bare, tabs.MucTab) if not tab and not muc: - tab = self.core.open_conversation_window(jid.full, focus=True) + tab = self.core.open_conversation_window(JID(jid.full), focus=True) elif muc: if jid.resource: tab = self.core.tabs.by_name_and_class(jid.full, @@ -956,7 +1313,7 @@ class CommandCore: else: self.core.focus_tab(tab) if len(args) == 2: - tab.command_say(args[1]) + await tab.command_say(args[1]) @command_args_parser.ignored def xml_tab(self): @@ -968,15 +1325,23 @@ class CommandCore: self.core.xml_tab = tab @command_args_parser.quoted(1) - def adhoc(self, args): + async def adhoc(self, args): if not args: return self.help('ad-hoc') - jid = safeJID(args[0]) + try: + jid = JID(args[0]) + except InvalidJID: + return self.core.information( + 'Invalid JID for ad-hoc command: %s' % args[0], + 'Error', + ) list_tab = tabs.AdhocCommandsListTab(self.core, jid) self.core.add_tab(list_tab, True) - cb = list_tab.on_list_received - self.core.xmpp.plugin['xep_0050'].get_commands( - jid=jid, local=False, callback=cb) + iq = await self.core.xmpp.plugin['xep_0050'].get_commands( + jid=jid, + local=False + ) + list_tab.on_list_received(iq) @command_args_parser.ignored def self_(self): @@ -990,7 +1355,7 @@ class CommandCore: info = ('Your JID is %s\nYour current status is "%s" (%s)' '\nYour default nickname is %s\nYou are running poezio %s' % (jid, message if message else '', show - if show else 'available', nick, config_opts.version)) + if show else 'available', nick, self.core.custom_version)) self.core.information(info, 'Info') @command_args_parser.ignored @@ -1000,6 +1365,16 @@ class CommandCore: """ self.core.reload_config() + @command_args_parser.raw + def debug(self, args): + """/debug [filename]""" + if not args.strip(): + config_module.setup_logging('') + self.core.information('Debug logging disabled!', 'Info') + elif args: + config_module.setup_logging(args) + self.core.information(f'Debug logging to {args} enabled!', 'Info') + def dumb_callback(*args, **kwargs): "mock callback" |