diff --git a/extensions/Channel_Interface_Conference.xml b/extensions/Channel_Interface_Conference.xml new file mode 100644 index 0000000..af3e627 --- /dev/null +++ b/extensions/Channel_Interface_Conference.xml @@ -0,0 +1,400 @@ + + + Copyright © 2009 Collabora Limited + Copyright © 2009 Nokia Corporation + +

This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version.

+ +

This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details.

+ +

You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301, USA.

+
+ + (draft 1) + + + + +

An interface for multi-user conference channels that can "continue + from" one or more individual channels.

+ + +

This interface addresses freedesktop.org bug + #24906 (GSM-compatible conference calls) and bug + #24939 (upgrading calls and chats to multi-user). + See those bugs for rationale and use cases.

+ +

Examples of usage:

+ +

Active and held GSM calls C1, C2 can be merged into a single + channel Cn with the Conference interface, by calling + CreateChannel({...ChannelType: ...Call, + ...InitialChannels: [C1, C2]}) + which returns Cn.

+ +

An XMPP 1-1 conversation C1 can be continued in a newly created + multi-user chatroom Cn by calling + CreateChannel({...ChannelType: ...Text, + ...InitialChannels: [C1]}) + which returns Cn.

+ +

An XMPP 1-1 conversation C1 can be continued in a specified + multi-user chatroom by calling + CreateChannel({...ChannelType: ...Text, ...HandleType: ROOM, + ...TargetID: 'telepathy@conf.example.com', + ...InitialChannels: [C1]}) + which returns a Conference channel.

+ +

Either of the XMPP cases could work for Call channels, to + upgrade from 1-1 Jingle to multi-user Muji. Any of the XMPP cases + could in principle work for link-local XMPP (XEP-0174).

+ +

The underlying switchboard representing an MSN 1-1 conversation C1 + with a contact X can be moved to a representation as a nameless + chatroom, Cn, to which more contacts can be invited, by calling + CreateChannel({...ChannelType: ...Text, + ...InitialChannels: [C1]}) + which returns Cn. C1 SHOULD remain open, with no underlying + switchboard attached. If X establishes a new switchboard with the + local user, C1 SHOULD pick up that switchboard rather than letting + it create a new channel. + [FIXME: should it?] + Similarly, if the local user sends a message in C1, then + a new switchboard to X should be created and associated with C1.

+ +

XMPP and MSN do not natively have a concept of merging two or more + channels C1, C2... into one channel, Cn. However, the GSM-style + merging API can be supported on XMPP and MSN, as an API short-cut + for upgrading C1 into a conference Cn (which invites the + TargetHandle of C1 into Cn), then immediately inviting the + TargetHandle of C2, the TargetHandle of C3, etc. into Cn as well.

+ +

With a suitable change of terminology, Skype has behaviour similar + to MSN.

+
+ +

The Group MAY have channel-specific handles for participants; + clients SHOULD support both Conferences that have channel-specific handles, + and those that do not.

+ + +

In the GSM case, the Conference's Group interface MAY have + channel-specific handles, to reflect the fact that the identities of + the participants might not be known - it can be possible to know that + there is another participant in the Conference, but not know who + they are. + [FIXME: fact check from GSM gurus needed] +

+ +

In the XMPP case, the Conference's Group interface SHOULD have + channel-specific handles, to reflect the fact that the participants + have MUC-specific identities, and the user might also be able to see + their global identities, or not.

+ +

In most other cases, including MSN and link-local XMPP, the + Conference's Group interface SHOULD NOT have channel-specific + handles, since users' identities are always visible.

+
+ +

Connection managers implementing channels with this interface + MUST NOT allow the object paths of channels that could be merged + into a Conference to be re-used, unless the channel re-using the + object path is equivalent to the channel that previously used it.

+ + +

If you upgrade some channels into a conference, and then close + the original channels, InitialChannels + (which is immutable) will contain paths to channels which no longer + exist. This implies that you should not re-use channel object paths, + unless future incarnations of the path are equivalent.

+ +

For instance, on protocols where you can only have + zero or one 1-1 text channels with Emily at one time, it would + be OK to re-use the same object path for every 1-1 text channel + with Emily; but on protocols where this is not true, it would + be misleading.

+
+ +
+ + + +

The individual Channels that + are continued by this conference, which have the same ChannelType as this one, but with TargetHandleType = CONTACT.

+ +

This property MUST NOT be requestable. + [FIXME: or would it be better for this one, and not IC, to be + requestable?] +

+ +

Change notification is via the + ChannelMerged and + ChannelRemoved signals.

+
+
+ + + +

Emitted when a new channel is added to the value of + Channels.

+
+ + + The channel that was added to + Channels. + +
+ + + +

Emitted when a channel is removed from the value of + Channels, either because it closed + or because it was split using the Splittable.DRAFT.Split method.

+ +

[FIXME: relative ordering of this vs. Closed? Do we + care?]

+
+ + + The channel that was removed from + Channels. + +
+ + + +

The initial value of Channels.

+ +

This property SHOULD be requestable. Omitting it from a request is + equivalent to providing it with an empty list as value. Requests + where its value has at least two elements SHOULD be expected to + succeed on any implementation of this interface.

+ +

Whether a request with 0 or 1 elements in the list will succeed is + indicated by SupportsNonMerges.

+ + +

In GSM, a pair of calls can be merged into a conference. In XMPP + and MSN, you can create a new chatroom, or upgrade one 1-1 channel + into a chatroom; however, on these protocols, it is also possible + to fake GSM-style merging by upgrading the first channel, then + inviting the targets of all the other channels into it.

+
+ +

If possible, the Channels' states SHOULD + NOT be altered by merging them into a conference. However, depending on + the protocol, the Channels MAY be placed in a "frozen" state by placing + them in this property's value or by calling + MergeableConference.DRAFT.Merge on them. + [FIXME: there's nothing in RequestableChannelClasses yet + to say what will happen, see #24906 comment 6]

+ + +

In Jingle, nothing special will happen to merged calls. UIs MAY + automatically place calls on hold before merging them, if that is + the desired behaviour; this SHOULD always work. Not doing + an implicit hold/unhold seems to preserve least-astonishment.

+ +

[FIXME: check whether ring supports faking Hold on both + channels, as it probably should: see #24906 comment 6] +

+ +

In GSM, the calls that are merged go into a state similar to + Hold, but they cannot be unheld, only split from the conference + call using Channel.Interface.Splittable.DRAFT.Split.

+
+ +

Depending on the protocol, it might be signalled to remote users + that this channel is a continuation of all the requested channels, + or that it is only a continuation of the first channel in the + list.

+ + +

In MSN, the conference steals the underlying switchboard (protocol + construct) from one of its component channels, so the conference + appears to remote users to be a continuation of that channel and no + other. The connection manager has to make some arbitrary choice, so + we arbitrarily mandate that it SHOULD choose the first channel in + the list as the one to continue.

+
+ +

This property is immutable.

+
+
+ + + +

A list of additional contacts invited to this conference when it + was created.

+ +

This property SHOULD be requestable, and appear in the allowed + properties in RequestableChannelClasses, in all connection + managers that can implement its semantics (in practice, this is + likely to mean exactly those connection managers where + SupportsNonMerges will be true).

+ +

If included in a request, the given contacts are automatically + invited into the new channel, as if they had been added with + Group.AddMembers(InitialInviteeHandles, + InvitationMessage immediately after + the channel was created.

+ + +

This is a simple convenience API for the common case that a UI + upgrades a 1-1 chat to a multi-user chat solely in order to invite + someone else to participate.

+
+ +

At most one of InitialInviteeHandles and InitialInviteeIDs may + appear in each request.

+ +

If the local user was not the initiator of this channel, the + Group.SelfHandle SHOULD appear in the value of this + property, together with any other contacts invited at the same time + (if that information is known).

+ +

This property is immutable.

+
+
+ + + +

A list of additional contacts invited to this conference when it + was created.

+ +

This property SHOULD be requestable, as an alternative to + InitialInviteeHandles. Its semantics + are the same, except that it takes a list of the string + representations of contact handles.

+ +

At most one of InitialInviteeHandles and InitialInviteeIDs may + appear in each request.

+ +

When a channel is created, the values of InitialInviteeHandles and + InitialInviteeIDs MUST correspond to each other.

+ +

This property is immutable.

+
+
+ + + +

The message that was sent to the + InitialInviteeHandles when they were + invited.

+ +

This property SHOULD be requestable, and appear in the allowed + properties in RequestableChannelClasses, in protocols where + invitations can have an accompanying text message.

+ + +

This allows invitations with a message to be sent when using + InitialInviteeHandles or + InitialInviteeIDs.

+
+ +

If the local user was not the initiator of this channel, the + message with which they were invited (if any) SHOULD appear in the + value of this property.

+ +

This property is immutable.

+
+
+ + + +

[FIXME: needs a better name; or perhaps it could be implied + by InitialInviteeHandles being requestable in XMPP/MSN but not in + GSM?]

+ +

If true, requests with InitialChannels + omitted, empty, or one element long should be expected to succeed.

+ +

This property SHOULD appear in RequestableChannelClasses for + conference channels if and only if its value on those channels will + be true.

+ + +

Putting this in RequestableChannelClasses means clients can find + out whether their request will succeed early enough to do + something about it.

+ +

In XMPP, you can request a channel of type ROOM without + incorporating any 1-1 chats at all - indeed, this is the normal + way to do it - or as a continuation of a single 1-1 chat, and then + invite other people in later.

+ +

The sense of this property is a bit awkward, but it avoids making it + an anti-capability. If the sense were inverted, then its presence in + RequestableChannelClasses would imply that the protocol lacks + a feature; as it stands, it is additive. (Contrast with + ImmutableStreams, which is the wrong way around for + backwards-compatibility reasons.)

+
+ +

If false, InitialChannels SHOULD be + supplied in all requests for this channel class, and contain at least + two channels. Requests where this requirement is not met SHOULD fail + with NotImplemented. +

+ + +

In GSM, you can only make a conference call by merging at least + two channels. + [FIXME: the CM could conceivably fake it, but that would be + rather nasty] +

+
+
+
+ +
+
diff --git a/extensions/all.xml b/extensions/all.xml index f9ca417..40eb5e7 100644 --- a/extensions/all.xml +++ b/extensions/all.xml @@ -44,6 +44,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA

+ diff --git a/src/muc-factory.c b/src/muc-factory.c index fd3b10d..cbe6f7a 100644 --- a/src/muc-factory.c +++ b/src/muc-factory.c @@ -39,6 +39,7 @@ #include "conn-olpc.h" #include "debug.h" #include "disco.h" +#include "im-channel.h" #include "message-util.h" #include "muc-channel.h" #include "namespaces.h" @@ -1437,6 +1438,210 @@ ensure_tubes_channel (GabbleMucFactory *self, } static gboolean +handle_conference_channel (GabbleMucFactory *self, + gpointer request_token, + GHashTable *request_properties, + gboolean require_new, + GError **error) +{ + GabbleMucFactoryPrivate *priv = self->priv; + DBusGConnection *bus = tp_get_bus (); + TpHandleSet *handles; + TpHandle room; + GabbleMucChannel *text_chan; + + TpHandleRepoIface *contact_handles = tp_base_connection_get_handles ( + TP_BASE_CONNECTION (priv->conn), TP_HANDLE_TYPE_CONTACT); + TpHandleRepoIface *room_handles = tp_base_connection_get_handles ( + TP_BASE_CONNECTION (priv->conn), TP_HANDLE_TYPE_ROOM); + + GPtrArray *initial_channels; + GArray *initial_handles; + char **initial_ids; + + initial_channels = tp_asv_get_boxed (request_properties, + GABBLE_IFACE_CHANNEL_INTERFACE_CONFERENCE ".InitialChannels", + TP_ARRAY_TYPE_OBJECT_PATH_LIST); + initial_handles = tp_asv_get_boxed (request_properties, + GABBLE_IFACE_CHANNEL_INTERFACE_CONFERENCE ".InitialInviteeHandles", + DBUS_TYPE_G_UINT_ARRAY); + initial_ids = tp_asv_get_boxed (request_properties, + GABBLE_IFACE_CHANNEL_INTERFACE_CONFERENCE ".InitialInviteeIDs", + G_TYPE_STRV); + + if (initial_handles != NULL && initial_ids != NULL) + { + g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "InitialInviteeHandles and InitialInviteeIDs must not both be given"); + return FALSE; + } + + handles = tp_handle_set_new (contact_handles); + + /* look at the list of initial channels, build a set of handles to invite */ + if (initial_channels != NULL) + { + guint i; + + for (i = 0; i < initial_channels->len; i++) + { + const char *object_path = g_ptr_array_index (initial_channels, i); + GObject *object; + TpHandle handle; + GabbleConnection *connection; + + object = dbus_g_connection_lookup_g_object (bus, object_path); + + /* FIXME: work with MUC channels in the future? */ + if (!GABBLE_IS_IM_CHANNEL (object)) + { + g_warning ("Channel %s is not an ImChannel, ignoring", + object_path); + continue; + } + + g_object_get (object, + "connection", &connection, + "handle", &handle, + NULL); + g_object_unref (connection); /* drop the ref immediately */ + + if (connection != priv->conn) + { + g_warning ("Channel %s is from a different Connection, ignoring", + object_path); + continue; + } + + tp_handle_set_add (handles, handle); + } + } + + /* look at the list of initial handles, add these to the handles set */ + if (initial_handles != NULL) + { + guint i; + + for (i = 0; i < initial_handles->len; i++) + { + TpHandle handle = g_array_index (initial_handles, TpHandle, i); + + if (tp_handle_inspect (contact_handles, handle) == NULL) + { + g_warning ("Bad Handle %u, ignoring", handle); + continue; + } + + tp_handle_set_add (handles, handle); + } + } + + /* look at the list of initial ids, add these to the handles set */ + if (initial_ids != NULL) + { + char **ptr; + + for (ptr = initial_ids; *ptr != NULL; ptr++) + { + char *id = *ptr; + TpHandle handle = tp_handle_ensure (contact_handles, id, NULL, NULL); + + if (handle == 0) + { + g_warning ("Bad ID '%s', ignoring", id); + continue; + } + + tp_handle_set_add (handles, handle); + tp_handle_unref (contact_handles, handle); + } + } + + /* FIXME: do we require google server PMUC jid? + * There's no super obvious way to tell.. you can't invite GMail users to + * a non-Google MUC (it just doesn't work), and if your own account is on a + * Google server, you may as well use a Google PMUC. If one of your initial + * contacts is using GMail, you should also use a Google PMUC */ + if (TRUE) + { + char *uuid, *id; + + /* N.B. gabble_generate_id() requires libuuid to generate valid UUIDs + * for Google PMUCs */ + uuid = gabble_generate_id (); + id = g_strdup_printf ("private-chat-%s@groupchat.google.com", uuid); + + room = tp_handle_ensure (room_handles, id, NULL, error); + if (room == 0) + { + return FALSE; + } + + g_free (uuid); + g_free (id); + } + + /* FIXME: MUC channel needs to expose Conference interface */ + if (ensure_muc_channel (self, priv, room, &text_chan, TRUE)) + { + if (require_new) + { + g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "That channel has already been created (or requested)"); + return FALSE; + } + else + { + tp_channel_manager_emit_request_already_satisfied (self, + request_token, TP_EXPORTABLE_CHANNEL (text_chan)); + } + } + else + { + gabble_muc_factory_associate_request (self, text_chan, + request_token); + } + + tp_handle_unref (room_handles, room); + + /* FIXME: include Self Handle to comply with spec ? */ + + { + GArray *array = tp_handle_set_to_array (handles); + const char *msg = tp_asv_get_string (request_properties, + GABBLE_IFACE_CHANNEL_INTERFACE_CONFERENCE ".InvitationMessage"); + guint i; + + if (msg == NULL) + { + /* FIXME: translate? */ + msg = "Please join our conversation"; + } + + for (i = 0; i < array->len; i++) + { + TpHandle handle = g_array_index (array, TpHandle, i); + const char *id = tp_handle_inspect (contact_handles, handle); + GError *error2 = NULL; + + gabble_muc_channel_send_invite (text_chan, id, msg, &error2); + if (error2 != NULL) + { + g_warning ("%s", error2->message); + g_error_free (error2); + continue; + } + } + + g_array_free (array, TRUE); + } + + tp_handle_set_destroy (handles); + + return TRUE; +} + +static gboolean handle_text_channel_request (GabbleMucFactory *self, gpointer request_token, GHashTable *request_properties, @@ -1668,16 +1873,26 @@ gabble_muc_factory_request (GabbleMucFactory *self, gboolean require_new) { GError *error = NULL; + TpHandleType handle_type; TpHandle handle; + gboolean conference; const gchar *channel_type; - if (tp_asv_get_uint32 (request_properties, - TP_IFACE_CHANNEL ".TargetHandleType", NULL) != TP_HANDLE_TYPE_ROOM) - return FALSE; - + handle_type = tp_asv_get_uint32 (request_properties, + TP_IFACE_CHANNEL ".TargetHandleType", NULL); channel_type = tp_asv_get_string (request_properties, TP_IFACE_CHANNEL ".ChannelType"); + /* Conference channels can be anonymous (HandleTypeNone) */ + conference = (handle_type == TP_HANDLE_TYPE_NONE && + !tp_strdiff (channel_type, TP_IFACE_CHANNEL_TYPE_TEXT) && + g_hash_table_lookup (request_properties, + GABBLE_IFACE_CHANNEL_INTERFACE_CONFERENCE ".InitialChannels") != NULL); + + /* the channel must either be a room, or a new conference */ + if (handle_type != TP_HANDLE_TYPE_ROOM && !conference) + return FALSE; + if (tp_strdiff (channel_type, TP_IFACE_CHANNEL_TYPE_TEXT) && tp_strdiff (channel_type, TP_IFACE_CHANNEL_TYPE_TUBES) && tp_strdiff (channel_type, TP_IFACE_CHANNEL_TYPE_STREAM_TUBE) && @@ -1687,9 +1902,15 @@ gabble_muc_factory_request (GabbleMucFactory *self, /* validity already checked by TpBaseConnection */ handle = tp_asv_get_uint32 (request_properties, TP_IFACE_CHANNEL ".TargetHandle", NULL); - g_assert (handle != 0); + g_assert (conference || handle != 0); - if (!tp_strdiff (channel_type, TP_IFACE_CHANNEL_TYPE_TEXT)) + if (conference) + { + if (handle_conference_channel (self, request_token, + request_properties, require_new, &error)) + return TRUE; + } + else if (!tp_strdiff (channel_type, TP_IFACE_CHANNEL_TYPE_TEXT)) { if (handle_text_channel_request (self, request_token, request_properties, require_new, handle, &error))