diff --git a/doc/source/commands.rst b/doc/source/commands.rst index f28f992f..054f6ccd 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -218,6 +218,13 @@ These commands work in *any* tab. /invitations Show the pending invitations. + /impromptu + **Usage:** ``/impromptu [jid ..]`` + + Invite specified JIDs into a newly created room. + + .. versionadded:: 0.13 + /activity **Usage:** ``/activity [ [specific] [comment]]`` @@ -472,6 +479,14 @@ Normal Conversation tab commands Get the software version of the current interlocutor (usually its XMPP client and Operating System). + /invite + **Usage:** ``/invite [jid ..]`` + + Invite specified JIDs, with this contact, into a newly + created room. + + .. versionadded:: 0.13 + .. _rostertab-commands: Contact list tab commands diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index 6baa6a27..da6d7954 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -81,6 +81,15 @@ and certificate validation. you know what you are doing, see the :ref:`ciphers` dedicated section for more details. + default_muc_service + + **Default value:** ``[empty]`` + + If specified, will be used instead of the MUC service provided by + the user domain. + + .. versionadded:: 0.13 + force_encryption **Default value:** ``true`` diff --git a/poezio/config.py b/poezio/config.py index a1f3dd49..d5a81c0e 100644 --- a/poezio/config.py +++ b/poezio/config.py @@ -49,6 +49,7 @@ DEFAULT_CONFIG = { 'custom_host': '', 'custom_port': '', 'default_nick': '', + 'default_muc_service': '', 'deterministic_nick_colors': True, 'device_id': '', 'nick_color_aliases': True, diff --git a/poezio/core/commands.py b/poezio/core/commands.py index 5c8199c0..86df9a93 100644 --- a/poezio/core/commands.py +++ b/poezio/core/commands.py @@ -6,6 +6,7 @@ import logging log = logging.getLogger(__name__) +import asyncio from xml.etree import cElementTree as ET from slixmpp.exceptions import XMPPError @@ -763,6 +764,24 @@ class CommandCore: self.core.invite(to.full, room, reason=reason) self.core.information('Invited %s to %s' % (to.bare, room), 'Info') + @command_args_parser.quoted(1, 0) + def impromptu(self, args: str) -> None: + """/impromptu [ ...]""" + + if args is None: + return self.help('impromptu') + + jids = set() + current_tab = self.core.tabs.current_tab + if isinstance(current_tab, tabs.ConversationTab): + jids.add(current_tab.general_jid) + + for jid in common.shell_split(' '.join(args)): + jids.add(safeJID(jid).bare) + + asyncio.ensure_future(self.core.impromptu(jids)) + self.core.information('Invited %s to a random room' % (' '.join(jids)), 'Info') + @command_args_parser.quoted(1, 1, ['']) def decline(self, args): """/decline [reason]""" diff --git a/poezio/core/completions.py b/poezio/core/completions.py index b283950e..87bb2d47 100644 --- a/poezio/core/completions.py +++ b/poezio/core/completions.py @@ -289,6 +289,19 @@ class CompletionCore: return Completion( the_input.new_completion, rooms, n, '', quotify=True) + def impromptu(self, the_input): + """Completion for /impromptu""" + n = the_input.get_argument_position(quoted=True) + onlines = [] + offlines = [] + for barejid in roster.jids(): + if len(roster[barejid]): + onlines.append(barejid) + else: + offlines.append(barejid) + comp = sorted(onlines) + sorted(offlines) + return Completion(the_input.new_completion, comp, n, quotify=True) + def activity(self, the_input): """Completion for /activity""" n = the_input.get_argument_position(quoted=True) diff --git a/poezio/core/core.py b/poezio/core/core.py index eec0d49b..2ab34412 100644 --- a/poezio/core/core.py +++ b/poezio/core/core.py @@ -13,12 +13,16 @@ import pipes import sys import shutil import time +import uuid from collections import defaultdict -from typing import Callable, Dict, List, Optional, Tuple, Type +from typing import Callable, Dict, List, Optional, Set, Tuple, Type +from xml.etree import cElementTree as ET +from functools import partial from slixmpp import JID from slixmpp.util import FileSystemPerJidCache from slixmpp.xmlstream.handler import Callback +from slixmpp.exceptions import IqError, IqTimeout from poezio import connection from poezio import decorators @@ -868,6 +872,85 @@ class Core: self.xmpp.plugin['xep_0030'].get_info( jid=jid, timeout=5, callback=callback) + def _impromptu_room_form(self, room): + fields = [ + ('hidden', 'FORM_TYPE', 'http://jabber.org/protocol/muc#roomconfig'), + ('boolean', 'muc#roomconfig_changesubject', True), + ('boolean', 'muc#roomconfig_allowinvites', True), + ('boolean', 'muc#roomconfig_persistent', True), + ('boolean', 'muc#roomconfig_membersonly', True), + ('boolean', 'muc#roomconfig_publicroom', False), + ('list-single', 'muc#roomconfig_whois', 'anyone'), + # MAM + ('boolean', 'muc#roomconfig_enablearchiving', True), # Prosody + ('boolean', 'mam', True), # Ejabberd community + ('boolean', 'muc#roomconfig_mam', True), # Ejabberd saas + ] + + form = self.xmpp['xep_0004'].make_form() + form['type'] = 'submit' + for field in fields: + form.add_field( + ftype=field[0], + var=field[1], + value=field[2], + ) + + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['to'] = room + query = ET.Element('{http://jabber.org/protocol/muc#owner}query') + query.append(form.xml) + iq.append(query) + return iq + + async def impromptu(self, jids: Set[JID]) -> None: + """ + Generates a new "Impromptu" room with a random localpart on the muc + component of the user who initiated the request. One the room is + created and the first user has joined, send invites for specified + contacts to join in. + """ + + results = await self.xmpp['xep_0030'].get_info_from_domain() + + muc_from_identity = '' + for info in results: + for identity in info['disco_info']['identities']: + if identity[0] == 'conference' and identity[1] == 'text': + muc_from_identity = info['from'].bare + + # Use config.default_muc_service as muc component if available, + # otherwise find muc component by disco#items-ing the user domain. + # If not, give up + default_muc = config.get('default_muc_service', muc_from_identity) + if not default_muc: + self.information( + "Error finding a MUC service to join. If your server does not " + "provide one, set 'default_muc_service' manually to a MUC " + "service that allows room creation.", + 'Error' + ) + return + + nick = self.own_nick + localpart = uuid.uuid4().hex + room = '{!s}@{!s}'.format(localpart, default_muc) + + self.open_new_room(room, nick).join() + iq = self._impromptu_room_form(room) + try: + await iq.send() + except (IqError, IqTimeout): + self.information('Failed to configure impromptu room.', 'Info') + # TODO: destroy? leave room. + return None + + self.information('Room %s created' % room, 'Info') + + for jid in jids: + self.invite(jid, room) + def get_error_message(self, stanza, deprecated: bool = False): """ Takes a stanza of the form @@ -1788,6 +1871,13 @@ class Core: desc='Invite jid in room with reason.', shortdesc='Invite someone in a room.', completion=self.completion.invite) + self.register_command( + 'impromptu', + self.command.impromptu, + usage=' [jid ...]', + desc='Invite specified JIDs into a newly created room.', + shortdesc='Invite specified JIDs into newly created room.', + completion=self.completion.impromptu) self.register_command( 'invitations', self.command.invitations, diff --git a/poezio/core/handlers.py b/poezio/core/handlers.py index 0e655d68..b87e7307 100644 --- a/poezio/core/handlers.py +++ b/poezio/core/handlers.py @@ -97,6 +97,11 @@ class HandlerCore: self.core.xmpp.plugin['xep_0030'].get_info( jid=self.core.xmpp.boundjid.domain, callback=callback) + def find_identities(self, _): + asyncio.ensure_future( + self.core.xmpp['xep_0030'].get_info_from_domain(), + ) + def on_carbon_received(self, message): """ Carbon received diff --git a/poezio/tabs/conversationtab.py b/poezio/tabs/conversationtab.py index 7e7a7488..94f1d719 100644 --- a/poezio/tabs/conversationtab.py +++ b/poezio/tabs/conversationtab.py @@ -79,6 +79,12 @@ class ConversationTab(OneToOneTab): ' allow you to see his presence, and allow them to' ' see your presence.', shortdesc='Add a user to your roster.') + self.register_command( + 'invite', + self.core.command.impromptu, + desc='Invite people into an impromptu room.', + shortdesc='Invite other users to the discussion', + completion=self.core.completion.impromptu) self.update_commands() self.update_keys()