/* * wocky-jingle-session.c - Source for WockyJingleSession * Copyright (C) 2008 Collabora Ltd. * * 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 St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "config.h" #include "wocky-jingle-session.h" #include #include #include #include #define WOCKY_DEBUG_FLAG WOCKY_DEBUG_JINGLE #include "wocky-debug-internal.h" #include "wocky-signals-marshal.h" #include "wocky-enumtypes.h" #include "wocky-jingle-content.h" #include "wocky-jingle-factory.h" /* FIXME: the RTP-specific bits of this file should be separated from the * generic Jingle code. */ #include "wocky-jingle-media-rtp.h" #include "wocky-namespaces.h" #include "wocky-node-private.h" #include "wocky-resource-contact.h" #include "wocky-utils.h" G_DEFINE_TYPE(WockyJingleSession, wocky_jingle_session, G_TYPE_OBJECT); /* signal enum */ enum { NEW_CONTENT, REMOTE_STATE_CHANGED, TERMINATED, CONTENT_REJECTED, QUERY_CAP, ABOUT_TO_INITIATE, LAST_SIGNAL }; static guint signals[LAST_SIGNAL] = {0}; /* properties */ enum { PROP_JINGLE_FACTORY = 1, PROP_PORTER, PROP_SESSION_ID, PROP_PEER_CONTACT, PROP_LOCAL_INITIATOR, PROP_STATE, PROP_DIALECT, PROP_LOCAL_HOLD, PROP_REMOTE_HOLD, PROP_REMOTE_RINGING, LAST_PROPERTY }; struct _WockyJingleSessionPrivate { /* Borrowed; the factory owns us. */ WockyJingleFactory *jingle_factory; WockyPorter *porter; WockyContact *peer_contact; /* Borrowed from peer_contact if it's a WockyResourceContact. */ const gchar *peer_resource; gchar *peer_jid; /* Either borrowed from 'porter' or equal to peer_jid. */ const gchar *initiator; gboolean local_initiator; /* WockyJingleContent objects keyed by content name. * Table owns references to these objects. */ GHashTable *initiator_contents; GHashTable *responder_contents; WockyJingleDialect dialect; WockyJingleState state; gchar *sid; gboolean locally_accepted; gboolean locally_terminated; gboolean local_hold; gboolean remote_hold; gboolean remote_ringing; gboolean dispose_has_run; }; typedef struct { WockyJingleState state; WockyJingleAction *actions; } WockyJingleStateActions; /* gcc should be able to figure this out from the table below, but.. */ #define MAX_ACTIONS_PER_STATE 12 /* NB: WOCKY_JINGLE_ACTION_UNKNOWN is used as a terminator here. */ static WockyJingleAction allowed_actions[WOCKY_N_JINGLE_STATES][MAX_ACTIONS_PER_STATE] = { /* WOCKY_JINGLE_STATE_PENDING_CREATED */ { WOCKY_JINGLE_ACTION_SESSION_INITIATE, WOCKY_JINGLE_ACTION_UNKNOWN }, /* WOCKY_JINGLE_STATE_PENDING_INITIATE_SENT */ { WOCKY_JINGLE_ACTION_SESSION_TERMINATE, WOCKY_JINGLE_ACTION_SESSION_ACCEPT, WOCKY_JINGLE_ACTION_TRANSPORT_ACCEPT, /* required for GTalk4 */ WOCKY_JINGLE_ACTION_DESCRIPTION_INFO, WOCKY_JINGLE_ACTION_SESSION_INFO, WOCKY_JINGLE_ACTION_TRANSPORT_INFO, WOCKY_JINGLE_ACTION_INFO, WOCKY_JINGLE_ACTION_UNKNOWN }, /* WOCKY_JINGLE_STATE_PENDING_INITIATED */ { WOCKY_JINGLE_ACTION_SESSION_ACCEPT, WOCKY_JINGLE_ACTION_SESSION_TERMINATE, WOCKY_JINGLE_ACTION_TRANSPORT_INFO, WOCKY_JINGLE_ACTION_CONTENT_REJECT, WOCKY_JINGLE_ACTION_CONTENT_MODIFY, WOCKY_JINGLE_ACTION_CONTENT_ACCEPT, WOCKY_JINGLE_ACTION_CONTENT_REMOVE, WOCKY_JINGLE_ACTION_DESCRIPTION_INFO, WOCKY_JINGLE_ACTION_TRANSPORT_ACCEPT, WOCKY_JINGLE_ACTION_SESSION_INFO, WOCKY_JINGLE_ACTION_INFO, WOCKY_JINGLE_ACTION_UNKNOWN }, /* WOCKY_JINGLE_STATE_PENDING_ACCEPT_SENT */ { WOCKY_JINGLE_ACTION_TRANSPORT_INFO, WOCKY_JINGLE_ACTION_DESCRIPTION_INFO, WOCKY_JINGLE_ACTION_SESSION_TERMINATE, WOCKY_JINGLE_ACTION_SESSION_INFO, WOCKY_JINGLE_ACTION_INFO, WOCKY_JINGLE_ACTION_UNKNOWN }, /* WOCKY_JINGLE_STATE_ACTIVE */ { WOCKY_JINGLE_ACTION_CONTENT_MODIFY, WOCKY_JINGLE_ACTION_CONTENT_ADD, WOCKY_JINGLE_ACTION_CONTENT_REMOVE, WOCKY_JINGLE_ACTION_CONTENT_REPLACE, WOCKY_JINGLE_ACTION_CONTENT_ACCEPT, WOCKY_JINGLE_ACTION_CONTENT_REJECT, WOCKY_JINGLE_ACTION_SESSION_INFO, WOCKY_JINGLE_ACTION_TRANSPORT_INFO, WOCKY_JINGLE_ACTION_DESCRIPTION_INFO, WOCKY_JINGLE_ACTION_INFO, WOCKY_JINGLE_ACTION_SESSION_TERMINATE, WOCKY_JINGLE_ACTION_UNKNOWN }, /* WOCKY_JINGLE_STATE_ENDED */ { WOCKY_JINGLE_ACTION_UNKNOWN } }; gboolean wocky_jingle_session_defines_action (WockyJingleSession *sess, WockyJingleAction a) { WockyJingleDialect d = sess->priv->dialect; if (a == WOCKY_JINGLE_ACTION_UNKNOWN) return FALSE; switch (d) { case WOCKY_JINGLE_DIALECT_V032: return TRUE; case WOCKY_JINGLE_DIALECT_V015: return (a != WOCKY_JINGLE_ACTION_DESCRIPTION_INFO && a != WOCKY_JINGLE_ACTION_SESSION_INFO); case WOCKY_JINGLE_DIALECT_GTALK4: if (a == WOCKY_JINGLE_ACTION_TRANSPORT_ACCEPT || a == WOCKY_JINGLE_ACTION_INFO ) return TRUE; case WOCKY_JINGLE_DIALECT_GTALK3: return (a == WOCKY_JINGLE_ACTION_SESSION_ACCEPT || a == WOCKY_JINGLE_ACTION_SESSION_INITIATE || a == WOCKY_JINGLE_ACTION_SESSION_TERMINATE || a == WOCKY_JINGLE_ACTION_TRANSPORT_INFO || a == WOCKY_JINGLE_ACTION_INFO); default: return FALSE; } } static void wocky_jingle_session_send_held (WockyJingleSession *sess); static void content_ready_cb (WockyJingleContent *c, gpointer user_data); static void content_removed_cb (WockyJingleContent *c, gpointer user_data); static void wocky_jingle_session_init (WockyJingleSession *obj) { WockyJingleSessionPrivate *priv = G_TYPE_INSTANCE_GET_PRIVATE (obj, WOCKY_TYPE_JINGLE_SESSION, WockyJingleSessionPrivate); obj->priv = priv; DEBUG ("Initializing the jingle session %p", obj); priv->initiator_contents = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); priv->responder_contents = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); priv->state = WOCKY_JINGLE_STATE_PENDING_CREATED; priv->locally_accepted = FALSE; priv->locally_terminated = FALSE; priv->dispose_has_run = FALSE; } static void dispose_content_hash ( WockyJingleSession *sess, GHashTable **contents) { GHashTableIter iter; gpointer content; g_hash_table_iter_init (&iter, *contents); while (g_hash_table_iter_next (&iter, NULL, &content)) { g_signal_handlers_disconnect_by_func (content, content_ready_cb, sess); g_signal_handlers_disconnect_by_func (content, content_removed_cb, sess); g_hash_table_iter_remove (&iter); } g_hash_table_unref (*contents); *contents = NULL; } static void wocky_jingle_session_dispose (GObject *object) { WockyJingleSession *sess = WOCKY_JINGLE_SESSION (object); WockyJingleSessionPrivate *priv = sess->priv; if (priv->dispose_has_run) return; DEBUG ("called"); priv->dispose_has_run = TRUE; g_assert ((priv->state == WOCKY_JINGLE_STATE_PENDING_CREATED) || (priv->state == WOCKY_JINGLE_STATE_ENDED)); dispose_content_hash (sess, &priv->initiator_contents); dispose_content_hash (sess, &priv->responder_contents); g_clear_object (&priv->peer_contact); g_clear_object (&priv->porter); g_free (priv->sid); priv->sid = NULL; g_free (priv->peer_jid); priv->peer_jid = NULL; if (G_OBJECT_CLASS (wocky_jingle_session_parent_class)->dispose) G_OBJECT_CLASS (wocky_jingle_session_parent_class)->dispose (object); } static void wocky_jingle_session_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { WockyJingleSession *sess = WOCKY_JINGLE_SESSION (object); WockyJingleSessionPrivate *priv = sess->priv; switch (property_id) { case PROP_JINGLE_FACTORY: g_value_set_object (value, priv->jingle_factory); break; case PROP_PORTER: g_value_set_object (value, priv->porter); break; case PROP_SESSION_ID: g_value_set_string (value, priv->sid); break; case PROP_LOCAL_INITIATOR: g_value_set_boolean (value, priv->local_initiator); break; case PROP_PEER_CONTACT: g_value_set_object (value, priv->peer_contact); break; case PROP_STATE: g_value_set_uint (value, priv->state); break; case PROP_DIALECT: g_value_set_uint (value, priv->dialect); break; case PROP_LOCAL_HOLD: g_value_set_boolean (value, priv->local_hold); break; case PROP_REMOTE_HOLD: g_value_set_boolean (value, priv->remote_hold); break; case PROP_REMOTE_RINGING: g_value_set_boolean (value, priv->remote_ringing); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void wocky_jingle_session_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { WockyJingleSession *sess = WOCKY_JINGLE_SESSION (object); WockyJingleSessionPrivate *priv = sess->priv; switch (property_id) { case PROP_JINGLE_FACTORY: priv->jingle_factory = g_value_get_object (value); g_assert (priv->jingle_factory != NULL); break; case PROP_PORTER: priv->porter = g_value_dup_object (value); g_assert (priv->porter != NULL); break; case PROP_SESSION_ID: g_free (priv->sid); priv->sid = g_value_dup_string (value); break; case PROP_LOCAL_INITIATOR: priv->local_initiator = g_value_get_boolean (value); break; case PROP_DIALECT: priv->dialect = g_value_get_uint (value); break; case PROP_PEER_CONTACT: priv->peer_contact = g_value_dup_object (value); break; case PROP_LOCAL_HOLD: { gboolean local_hold = g_value_get_boolean (value); if (priv->local_hold != local_hold) { priv->local_hold = local_hold; if (priv->state >= WOCKY_JINGLE_STATE_PENDING_INITIATED && priv->state < WOCKY_JINGLE_STATE_ENDED) wocky_jingle_session_send_held (sess); /* else, we'll send this in set_state when we move to PENDING_INITIATED or * better. */ } break; } default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); g_assert_not_reached (); break; } } static void wocky_jingle_session_constructed (GObject *object) { void (*chain_up) (GObject *) = G_OBJECT_CLASS (wocky_jingle_session_parent_class)->constructed; WockyJingleSession *self = WOCKY_JINGLE_SESSION (object); WockyJingleSessionPrivate *priv = self->priv; if (chain_up != NULL) chain_up (object); g_assert (priv->jingle_factory != NULL); g_assert (priv->porter != NULL); g_assert (priv->peer_contact != NULL); g_assert (priv->sid != NULL); priv->peer_jid = wocky_contact_dup_jid (priv->peer_contact); if (priv->local_initiator) priv->initiator = wocky_porter_get_full_jid (priv->porter); else priv->initiator = priv->peer_jid; if (WOCKY_IS_RESOURCE_CONTACT (priv->peer_contact)) priv->peer_resource = wocky_resource_contact_get_resource ( WOCKY_RESOURCE_CONTACT (priv->peer_contact)); } WockyJingleSession * wocky_jingle_session_new ( WockyJingleFactory *factory, WockyPorter *porter, const gchar *session_id, gboolean local_initiator, WockyContact *peer, WockyJingleDialect dialect, gboolean local_hold) { return g_object_new (WOCKY_TYPE_JINGLE_SESSION, "session-id", session_id, "jingle-factory", factory, "porter", porter, "local-initiator", local_initiator, "peer-contact", peer, "dialect", dialect, "local-hold", local_hold, NULL); } static void wocky_jingle_session_class_init (WockyJingleSessionClass *cls) { GObjectClass *object_class = G_OBJECT_CLASS (cls); GParamSpec *param_spec; g_type_class_add_private (cls, sizeof (WockyJingleSessionPrivate)); object_class->constructed = wocky_jingle_session_constructed; object_class->get_property = wocky_jingle_session_get_property; object_class->set_property = wocky_jingle_session_set_property; object_class->dispose = wocky_jingle_session_dispose; /* property definitions */ param_spec = g_param_spec_object ("jingle-factory", "WockyJingleFactory object", "The Jingle factory which created this session", WOCKY_TYPE_JINGLE_FACTORY, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_property (object_class, PROP_JINGLE_FACTORY, param_spec); param_spec = g_param_spec_object ("porter", "WockyPorter", "The WockyPorter for the current connection", WOCKY_TYPE_PORTER, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_property (object_class, PROP_PORTER, param_spec); param_spec = g_param_spec_string ("session-id", "Session ID", "A unique session identifier used throughout all communication.", NULL, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_property (object_class, PROP_SESSION_ID, param_spec); param_spec = g_param_spec_boolean ("local-initiator", "Session initiator", "Specifies if local end initiated the session.", TRUE, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_property (object_class, PROP_LOCAL_INITIATOR, param_spec); /** * WockyJingleSession:peer-contact: * * The #WockyContact representing the other party in the session. Note that * if this is a #WockyBareContact (as opposed to a #WockyResourceContact) the * session is with the contact's bare JID. */ param_spec = g_param_spec_object ("peer-contact", "Session peer", "The WockyContact representing the other party in the session.", WOCKY_TYPE_CONTACT, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_property (object_class, PROP_PEER_CONTACT, param_spec); param_spec = g_param_spec_uint ("state", "Session state", "The current state that the session is in.", 0, G_MAXUINT32, WOCKY_JINGLE_STATE_PENDING_CREATED, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_property (object_class, PROP_STATE, param_spec); param_spec = g_param_spec_uint ("dialect", "Jingle dialect", "Jingle dialect used for this session.", 0, G_MAXUINT32, WOCKY_JINGLE_DIALECT_ERROR, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_property (object_class, PROP_DIALECT, param_spec); param_spec = g_param_spec_boolean ("local-hold", "Local hold", "TRUE if we've placed the peer on hold", FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_property (object_class, PROP_LOCAL_HOLD, param_spec); param_spec = g_param_spec_boolean ("remote-hold", "Remote hold", "TRUE if the peer has placed us on hold", FALSE, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); g_object_class_install_property (object_class, PROP_REMOTE_HOLD, param_spec); param_spec = g_param_spec_boolean ("remote-ringing", "Remote ringing", "TRUE if the peer's client is ringing", FALSE, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); g_object_class_install_property (object_class, PROP_REMOTE_RINGING, param_spec); /* signal definitions */ signals[NEW_CONTENT] = g_signal_new ("new-content", G_TYPE_FROM_CLASS (cls), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, G_TYPE_OBJECT); /** * WockyJingleSession::terminated: * @session: the session * @locally_terminated: %TRUE if the session ended due to a call to * wocky_jingle_session_terminate(); %FALSE if the peer ended the session. * @reason: a #WockyJingleReason describing why the session terminated * @text: a possibly-%NULL human-readable string describing why the session * terminated * * Emitted when the session ends, just after #WockyJingleSession:state moves * to #WOCKY_JINGLE_STATE_ENDED. */ signals[TERMINATED] = g_signal_new ("terminated", G_TYPE_FROM_CLASS (cls), G_SIGNAL_RUN_LAST, 0, NULL, NULL, _wocky_signals_marshal_VOID__BOOLEAN_UINT_STRING, G_TYPE_NONE, 3, G_TYPE_BOOLEAN, G_TYPE_UINT, G_TYPE_STRING); signals[REMOTE_STATE_CHANGED] = g_signal_new ("remote-state-changed", G_TYPE_FROM_CLASS (cls), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); signals[CONTENT_REJECTED] = g_signal_new ("content-rejected", G_TYPE_FROM_CLASS (cls), G_SIGNAL_RUN_LAST, 0, NULL, NULL, _wocky_signals_marshal_VOID__OBJECT_UINT_STRING, G_TYPE_NONE, 3, G_TYPE_OBJECT, G_TYPE_UINT, G_TYPE_STRING); /* * @contact: this call's peer (the artist commonly known as * wocky_jingle_session_get_peer_contact()) * @cap: the XEP-0115 feature string the session is interested in. * * Emitted when the session wants to check whether the peer has a particular * capability. The handler should return %TRUE if @contact has @cap. */ signals[QUERY_CAP] = g_signal_new ("query-cap", G_TYPE_FROM_CLASS (cls), G_SIGNAL_RUN_LAST, 0, g_signal_accumulator_first_wins, NULL, _wocky_signals_marshal_BOOLEAN__OBJECT_STRING, G_TYPE_BOOLEAN, 2, WOCKY_TYPE_CONTACT, G_TYPE_STRING); signals[ABOUT_TO_INITIATE] = g_signal_new ("about-to-initiate", G_TYPE_FROM_CLASS (cls), G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); } typedef void (*HandlerFunc)(WockyJingleSession *sess, WockyNode *node, GError **error); typedef void (*ContentHandlerFunc)(WockyJingleSession *sess, WockyJingleContent *c, WockyNode *content_node, gpointer user_data, GError **error); static gboolean extract_reason (WockyNode *node, WockyJingleReason *reason, gchar **message) { WockyJingleReason _reason = WOCKY_JINGLE_REASON_UNKNOWN; WockyNode *child; WockyNodeIter iter; g_return_val_if_fail (node != NULL, FALSE); if (message != NULL) *message = g_strdup (wocky_node_get_content_from_child (node, "text")); wocky_node_iter_init (&iter, node, NULL, NULL); while (wocky_node_iter_next (&iter, &child)) { if (wocky_enum_from_nick ( wocky_jingle_reason_get_type (), child->name, (gint *) &_reason)) { if (reason != NULL) *reason = _reason; return TRUE; } } return FALSE; } static WockyJingleAction parse_action (const gchar *txt) { if (txt == NULL) return WOCKY_JINGLE_ACTION_UNKNOWN; /* synonyms, best deal with them right now */ if (!wocky_strdiff (txt, "initiate") || !wocky_strdiff (txt, "session-initiate")) return WOCKY_JINGLE_ACTION_SESSION_INITIATE; else if (!wocky_strdiff (txt, "terminate") || !wocky_strdiff (txt, "session-terminate") || !wocky_strdiff (txt, "reject")) return WOCKY_JINGLE_ACTION_SESSION_TERMINATE; else if (!wocky_strdiff (txt, "accept") || !wocky_strdiff (txt, "session-accept")) return WOCKY_JINGLE_ACTION_SESSION_ACCEPT; else if (!wocky_strdiff (txt, "candidates") || !wocky_strdiff (txt, "transport-info")) return WOCKY_JINGLE_ACTION_TRANSPORT_INFO; else if (!wocky_strdiff (txt, "content-accept")) return WOCKY_JINGLE_ACTION_CONTENT_ACCEPT; else if (!wocky_strdiff (txt, "content-add")) return WOCKY_JINGLE_ACTION_CONTENT_ADD; else if (!wocky_strdiff (txt, "content-modify")) return WOCKY_JINGLE_ACTION_CONTENT_MODIFY; else if (!wocky_strdiff (txt, "content-replace")) return WOCKY_JINGLE_ACTION_CONTENT_REPLACE; else if (!wocky_strdiff (txt, "content-reject")) return WOCKY_JINGLE_ACTION_CONTENT_REJECT; else if (!wocky_strdiff (txt, "content-remove")) return WOCKY_JINGLE_ACTION_CONTENT_REMOVE; else if (!wocky_strdiff (txt, "session-info")) return WOCKY_JINGLE_ACTION_SESSION_INFO; else if (!wocky_strdiff (txt, "transport-accept")) return WOCKY_JINGLE_ACTION_TRANSPORT_ACCEPT; else if (!wocky_strdiff (txt, "description-info")) return WOCKY_JINGLE_ACTION_DESCRIPTION_INFO; else if (!wocky_strdiff (txt, "info")) return WOCKY_JINGLE_ACTION_INFO; return WOCKY_JINGLE_ACTION_UNKNOWN; } static const gchar * produce_action (WockyJingleAction action, WockyJingleDialect dialect) { gboolean gmode = (dialect == WOCKY_JINGLE_DIALECT_GTALK3) || (dialect == WOCKY_JINGLE_DIALECT_GTALK4); g_return_val_if_fail (action != WOCKY_JINGLE_ACTION_UNKNOWN, NULL); switch (action) { case WOCKY_JINGLE_ACTION_SESSION_INITIATE: return (gmode) ? "initiate" : "session-initiate"; case WOCKY_JINGLE_ACTION_SESSION_TERMINATE: return (gmode) ? "terminate" : "session-terminate"; case WOCKY_JINGLE_ACTION_SESSION_ACCEPT: return (gmode) ? "accept" : "session-accept"; case WOCKY_JINGLE_ACTION_TRANSPORT_INFO: return (dialect == WOCKY_JINGLE_DIALECT_GTALK3) ? "candidates" : "transport-info"; case WOCKY_JINGLE_ACTION_CONTENT_ACCEPT: return "content-accept"; case WOCKY_JINGLE_ACTION_CONTENT_ADD: return "content-add"; case WOCKY_JINGLE_ACTION_CONTENT_MODIFY: return "content-modify"; case WOCKY_JINGLE_ACTION_CONTENT_REMOVE: return "content-remove"; case WOCKY_JINGLE_ACTION_CONTENT_REPLACE: return "content-replace"; case WOCKY_JINGLE_ACTION_CONTENT_REJECT: return "content-reject"; case WOCKY_JINGLE_ACTION_SESSION_INFO: return "session-info"; case WOCKY_JINGLE_ACTION_TRANSPORT_ACCEPT: return "transport-accept"; case WOCKY_JINGLE_ACTION_DESCRIPTION_INFO: return "description-info"; case WOCKY_JINGLE_ACTION_INFO: return "info"; default: /* only reached if g_return_val_if_fail is disabled */ DEBUG ("unknown action %u", action); return NULL; } } static gboolean action_is_allowed (WockyJingleAction action, WockyJingleState state) { guint i; for (i = 0; allowed_actions[state][i] != WOCKY_JINGLE_ACTION_UNKNOWN; i++) { if (allowed_actions[state][i] == action) return TRUE; } return FALSE; } static void wocky_jingle_session_send_rtp_info (WockyJingleSession *sess, const gchar *name); static void set_state (WockyJingleSession *sess, WockyJingleState state, WockyJingleReason termination_reason, const gchar *text); static WockyJingleContent *_get_any_content (WockyJingleSession *session); gboolean wocky_jingle_session_peer_has_cap ( WockyJingleSession *self, const gchar *cap_or_quirk) { gboolean ret; g_return_val_if_fail (WOCKY_IS_JINGLE_SESSION (self), FALSE); g_return_val_if_fail (cap_or_quirk != NULL, FALSE); g_signal_emit (self, signals[QUERY_CAP], 0, self->priv->peer_contact, cap_or_quirk, &ret); return ret; } static gboolean lookup_content (WockyJingleSession *sess, const gchar *name, const gchar *creator, gboolean fail_if_missing, WockyJingleContent **c, GError **error) { WockyJingleSessionPrivate *priv = sess->priv; if (name == NULL) { g_set_error (error, WOCKY_XMPP_ERROR, WOCKY_XMPP_ERROR_BAD_REQUEST, "'name' attribute unset"); return FALSE; } if (WOCKY_JINGLE_DIALECT_IS_GOOGLE (priv->dialect)) { /* Only the initiator can create contents on GTalk. */ *c = g_hash_table_lookup (priv->initiator_contents, name); } else { /* Versions of Gabble between 0.7.16 and 0.7.28 (inclusive) omitted the * 'creator' attribute from transport-info (and possibly other) stanzas. * We try to detect contacts using such a version of Gabble from their * caps; if 'creator' is missing and the peer has that caps flag, we look * up the content in both hashes. * * While this doesn't deal with the case where the content is found in * both hashes, this isn't a problem in practice: the versions of Gabble * we're working around didn't allow this to happen (they'd either reject * the second stream, or let it replace the first, depending on the phase * of the moon, and get kind of confused in the process), and we try to * pick globally-unique content names. */ if (creator == NULL && wocky_jingle_session_peer_has_cap (sess, WOCKY_QUIRK_OMITS_CONTENT_CREATORS)) { DEBUG ("working around missing 'creator' attribute"); *c = g_hash_table_lookup (priv->initiator_contents, name); if (*c == NULL) *c = g_hash_table_lookup (priv->responder_contents, name); } else if (!wocky_strdiff (creator, "initiator")) { *c = g_hash_table_lookup (priv->initiator_contents, name); } else if (!wocky_strdiff (creator, "responder")) { *c = g_hash_table_lookup (priv->responder_contents, name); } else { g_set_error (error, WOCKY_XMPP_ERROR, WOCKY_XMPP_ERROR_BAD_REQUEST, "'creator' attribute %s", (creator == NULL ? "missing" : "invalid")); return FALSE; } } if (fail_if_missing && *c == NULL) { g_set_error (error, WOCKY_XMPP_ERROR, WOCKY_XMPP_ERROR_BAD_REQUEST, "Content '%s' (created by %s) does not exist", name, creator); return FALSE; } return TRUE; } static void _foreach_content (WockyJingleSession *sess, WockyNode *node, gboolean fail_if_missing, ContentHandlerFunc func, gpointer user_data, GError **error) { WockyJingleContent *c; WockyNode *content_node; WockyNodeIter iter; wocky_node_iter_init (&iter, node, "content", NULL); while (wocky_node_iter_next (&iter, &content_node)) { if (!lookup_content (sess, wocky_node_get_attribute (content_node, "name"), wocky_node_get_attribute (content_node, "creator"), fail_if_missing, &c, error)) return; func (sess, c, content_node, user_data, error); if (*error != NULL) return; } } struct idle_content_reject_ctx { WockyJingleSession *session; WockyStanza *msg; }; static gboolean idle_content_reject (gpointer data) { struct idle_content_reject_ctx *ctx = data; wocky_jingle_session_send (ctx->session, ctx->msg); g_object_unref (ctx->session); g_free (ctx); return FALSE; } static void fire_idle_content_reject (WockyJingleSession *sess, const gchar *name, const gchar *creator) { struct idle_content_reject_ctx *ctx = g_new0 (struct idle_content_reject_ctx, 1); WockyNode *sess_node, *node; if (creator == NULL) creator = ""; ctx->session = g_object_ref (sess); ctx->msg = wocky_jingle_session_new_message (ctx->session, WOCKY_JINGLE_ACTION_CONTENT_REJECT, &sess_node); g_debug ("name = %s, initiator = %s", name, creator); node = wocky_node_add_child (sess_node, "content"); wocky_node_set_attributes (node, "name", name, "creator", creator, NULL); /* FIXME: add API for ordering IQs rather than using g_idle_add. */ g_idle_add (idle_content_reject, ctx); } static WockyJingleContent * create_content (WockyJingleSession *sess, GType content_type, WockyJingleMediaType type, WockyJingleContentSenders senders, const gchar *content_ns, const gchar *transport_ns, const gchar *name, WockyNode *content_node, GError **error) { WockyJingleSessionPrivate *priv = sess->priv; WockyJingleContent *c; GHashTable *contents; DEBUG ("session creating new content name %s, type %d", name, type); /* FIXME: media-type is introduced by WockyJingleMediaRTP, not by the * superclass, so this call is unsafe in the general case */ c = g_object_new (content_type, "session", sess, "content-ns", content_ns, "transport-ns", transport_ns, "media-type", type, "name", name, "disposition", "session", "senders", senders, NULL); g_signal_connect (c, "ready", (GCallback) content_ready_cb, sess); g_signal_connect (c, "removed", (GCallback) content_removed_cb, sess); /* if we are called by parser, parse content add */ if (content_node != NULL) { wocky_jingle_content_parse_add (c, content_node, WOCKY_JINGLE_DIALECT_IS_GOOGLE (priv->dialect), error); if (*error != NULL) { g_object_unref (c); return NULL; } /* gtalk streams don't have name, so use whatever Content came up with */ if (name == NULL) name = wocky_jingle_content_get_name (c); } if (priv->local_initiator == wocky_jingle_content_is_created_by_us (c)) { DEBUG ("inserting content %s into initiator_contents", name); contents = priv->initiator_contents; } else { DEBUG ("inserting content %s into responder_contents", name); contents = priv->responder_contents; } /* If the content already existed, either we shouldn't have picked the name * we did (if we're creating it) or _each_content_add should have already * said no. */ g_assert (g_hash_table_lookup (contents, name) == NULL); g_hash_table_insert (contents, g_strdup (name), c); g_signal_emit (sess, signals[NEW_CONTENT], 0, c); return c; } static void _each_content_add (WockyJingleSession *sess, WockyJingleContent *c, WockyNode *content_node, gpointer user_data, GError **error) { WockyJingleSessionPrivate *priv = sess->priv; const gchar *name = wocky_node_get_attribute (content_node, "name"); WockyNode *desc_node = wocky_node_get_child (content_node, "description"); GType content_type = 0; const gchar *content_ns = NULL; if (desc_node != NULL) { content_ns = wocky_node_get_ns (desc_node); DEBUG ("namespace: %s", content_ns); content_type = wocky_jingle_factory_lookup_content_type ( wocky_jingle_session_get_factory (sess), content_ns); } if (content_type == 0) { /* if this is session-initiate, we should return error, otherwise, * we should respond with content-reject */ if (priv->state < WOCKY_JINGLE_STATE_PENDING_INITIATED) g_set_error (error, WOCKY_XMPP_ERROR, WOCKY_XMPP_ERROR_BAD_REQUEST, "unsupported content type with ns %s", content_ns); else fire_idle_content_reject (sess, name, wocky_node_get_attribute (content_node, "creator")); return; } if (c != NULL) { g_set_error (error, WOCKY_XMPP_ERROR, WOCKY_XMPP_ERROR_BAD_REQUEST, "content '%s' already exists", name); return; } create_content (sess, content_type, WOCKY_JINGLE_MEDIA_TYPE_NONE, WOCKY_JINGLE_CONTENT_SENDERS_BOTH, content_ns, NULL, NULL, content_node, error); } static void _each_content_remove (WockyJingleSession *sess, WockyJingleContent *c, WockyNode *content_node, gpointer user_data, GError **error) { g_assert (c != NULL); wocky_jingle_content_remove (c, FALSE); } static void _each_content_rejected (WockyJingleSession *sess, WockyJingleContent *c, WockyNode *content_node, gpointer user_data, GError **error) { WockyJingleReason reason = GPOINTER_TO_UINT (user_data); g_assert (c != NULL); g_signal_emit (sess, signals[CONTENT_REJECTED], 0, c, reason, ""); wocky_jingle_content_remove (c, FALSE); } static void _each_content_modify (WockyJingleSession *sess, WockyJingleContent *c, WockyNode *content_node, gpointer user_data, GError **error) { g_assert (c != NULL); wocky_jingle_content_update_senders (c, content_node, error); if (*error != NULL) return; } static void _each_content_replace (WockyJingleSession *sess, WockyJingleContent *c, WockyNode *content_node, gpointer user_data, GError **error) { _each_content_remove (sess, c, content_node, NULL, error); if (*error != NULL) return; _each_content_add (sess, c, content_node, NULL, error); } static void _each_content_accept (WockyJingleSession *sess, WockyJingleContent *c, WockyNode *content_node, gpointer user_data, GError **error) { WockyJingleSessionPrivate *priv = sess->priv; WockyJingleContentState state; g_assert (c != NULL); g_object_get (c, "state", &state, NULL); if (state != WOCKY_JINGLE_CONTENT_STATE_SENT) { #ifdef ENABLE_DEBUG const gchar *name = wocky_node_get_attribute (content_node, "name"); DEBUG ("ignoring content \"%s\"s acceptance for content not in SENT state", name); #endif return; } wocky_jingle_content_parse_accept (c, content_node, WOCKY_JINGLE_DIALECT_IS_GOOGLE (priv->dialect), error); } static void _each_description_info (WockyJingleSession *sess, WockyJingleContent *c, WockyNode *content_node, gpointer user_data, GError **error) { wocky_jingle_content_parse_description_info (c, content_node, error); } static void on_session_initiate (WockyJingleSession *sess, WockyNode *node, GError **error) { WockyJingleSessionPrivate *priv = sess->priv; /* we can't call ourselves at the moment */ if (priv->local_initiator) { /* We ignore initiate from us, and terminate the session immediately * afterwards */ wocky_jingle_session_terminate (sess, WOCKY_JINGLE_REASON_BUSY, NULL, NULL); return; } if ((priv->dialect == WOCKY_JINGLE_DIALECT_GTALK3)) { const gchar *content_ns = NULL; WockyNode *desc_node = wocky_node_get_child (node, "description"); content_ns = wocky_node_get_ns (desc_node); if (!wocky_strdiff (content_ns, WOCKY_XMPP_NS_GOOGLE_SESSION_VIDEO)) { WockyJingleFactory *factory = wocky_jingle_session_get_factory (sess); GType content_type = 0; DEBUG ("GTalk v3 session with audio and video"); /* audio and video content */ content_type = wocky_jingle_factory_lookup_content_type ( factory, content_ns); create_content (sess, content_type, WOCKY_JINGLE_MEDIA_TYPE_VIDEO, WOCKY_JINGLE_CONTENT_SENDERS_BOTH, WOCKY_XMPP_NS_GOOGLE_SESSION_VIDEO, NULL, "video", node, error); content_type = wocky_jingle_factory_lookup_content_type ( factory, WOCKY_XMPP_NS_GOOGLE_SESSION_PHONE); create_content (sess, content_type, WOCKY_JINGLE_MEDIA_TYPE_AUDIO, WOCKY_JINGLE_CONTENT_SENDERS_BOTH, WOCKY_XMPP_NS_GOOGLE_SESSION_PHONE, NULL, "audio", node, error); } else { _each_content_add (sess, NULL, node, NULL, error); } } else if (priv->dialect == WOCKY_JINGLE_DIALECT_GTALK4) { /* in this case we implicitly have just one content */ _each_content_add (sess, NULL, node, NULL, error); } else { _foreach_content (sess, node, FALSE, _each_content_add, NULL, error); } if (*error == NULL) { /* FIXME: contents defined here should always have "session" content * disposition; resolve this as soon as the proper procedure is defined * in XEP-0166. */ set_state (sess, WOCKY_JINGLE_STATE_PENDING_INITIATED, WOCKY_JINGLE_REASON_UNKNOWN, NULL); wocky_jingle_session_send_rtp_info (sess, "ringing"); } } static void on_content_add (WockyJingleSession *sess, WockyNode *node, GError **error) { _foreach_content (sess, node, FALSE, _each_content_add, NULL, error); } static void on_content_modify (WockyJingleSession *sess, WockyNode *node, GError **error) { _foreach_content (sess, node, TRUE, _each_content_modify, NULL, error); } static void on_content_remove (WockyJingleSession *sess, WockyNode *node, GError **error) { _foreach_content (sess, node, TRUE, _each_content_remove, NULL, error); } static void on_content_replace (WockyJingleSession *sess, WockyNode *node, GError **error) { _foreach_content (sess, node, TRUE, _each_content_replace, NULL, error); } static void on_content_reject (WockyJingleSession *sess, WockyNode *node, GError **error) { WockyNode *n = wocky_node_get_child (node, "reason"); WockyJingleReason reason = WOCKY_JINGLE_REASON_UNKNOWN; DEBUG (" "); if (n != NULL) extract_reason (n, &reason, NULL); if (reason == WOCKY_JINGLE_REASON_UNKNOWN) reason = WOCKY_JINGLE_REASON_GENERAL_ERROR; _foreach_content (sess, node, TRUE, _each_content_rejected, GUINT_TO_POINTER (reason), error); } static void on_content_accept (WockyJingleSession *sess, WockyNode *node, GError **error) { _foreach_content (sess, node, TRUE, _each_content_accept, NULL, error); } static void on_session_accept (WockyJingleSession *sess, WockyNode *node, GError **error) { WockyJingleSessionPrivate *priv = sess->priv; DEBUG ("called"); if ((priv->dialect == WOCKY_JINGLE_DIALECT_GTALK3) || (priv->dialect == WOCKY_JINGLE_DIALECT_GTALK4)) { /* Google Talk calls don't have contents per se; they just have * s in different namespaces for audio and video, in the * same stanza. So we need to feed the whole stanza to each * content in turn. */ GList *cs = wocky_jingle_session_get_contents (sess); GList *l; for (l = cs; l != NULL; l = l->next) _each_content_accept (sess, l->data, node, NULL, error); g_list_free (cs); } else { _foreach_content (sess, node, TRUE, _each_content_accept, NULL, error); } if (*error != NULL) return; set_state (sess, WOCKY_JINGLE_STATE_ACTIVE, WOCKY_JINGLE_REASON_UNKNOWN, NULL); /* Make sure each content knows the session is active */ g_list_foreach (wocky_jingle_session_get_contents (sess), (GFunc) g_object_notify, "state"); if (priv->dialect != WOCKY_JINGLE_DIALECT_V032) { /* If this is a dialect that doesn't support , we treat * session-accept as the cue to remove the ringing flag. */ priv->remote_ringing = FALSE; g_signal_emit (sess, signals[REMOTE_STATE_CHANGED], 0); } } static void mute_all_foreach (gpointer key, gpointer value, gpointer mute) { if (G_OBJECT_TYPE (value) == WOCKY_TYPE_JINGLE_MEDIA_RTP) g_object_set (value, "remote-mute", GPOINTER_TO_INT (mute), NULL); } static void mute_all (WockyJingleSession *sess, gboolean mute) { g_hash_table_foreach (sess->priv->initiator_contents, mute_all_foreach, GINT_TO_POINTER (mute)); g_hash_table_foreach (sess->priv->responder_contents, mute_all_foreach, GINT_TO_POINTER (mute)); } static gboolean set_mute (WockyJingleSession *sess, const gchar *name, const gchar *creator, gboolean mute, GError **error) { WockyJingleContent *c; if (name == NULL) { mute_all (sess, mute); return TRUE; } if (!lookup_content (sess, name, creator, TRUE /* fail if missing */, &c, error)) return FALSE; if (G_OBJECT_TYPE (c) != WOCKY_TYPE_JINGLE_MEDIA_RTP) { g_set_error (error, WOCKY_XMPP_ERROR, WOCKY_XMPP_ERROR_BAD_REQUEST, "content '%s' isn't an RTP session", name); return FALSE; } g_object_set (c, "remote-mute", mute, NULL); return TRUE; } static void set_hold (WockyJingleSession *sess, gboolean hold) { sess->priv->remote_hold = hold; } static void set_ringing (WockyJingleSession *sess, gboolean ringing) { sess->priv->remote_ringing = ringing; } static gboolean handle_payload (WockyJingleSession *sess, WockyNode *payload, gboolean *handled, GError **error) { const gchar *ns = wocky_node_get_ns (payload); const gchar *elt = payload->name; const gchar *name = wocky_node_get_attribute (payload, "name"); const gchar *creator = wocky_node_get_attribute (payload, "creator"); if (wocky_strdiff (ns, WOCKY_XMPP_NS_JINGLE_RTP_INFO)) { *handled = FALSE; return TRUE; } *handled = TRUE; if (!wocky_strdiff (elt, "active")) { /* Clear all states, we're active */ mute_all (sess, FALSE); set_ringing (sess, FALSE); set_hold (sess, FALSE); } else if (!wocky_strdiff (elt, "ringing")) { set_ringing (sess, TRUE); } else if (!wocky_strdiff (elt, "hold")) { set_hold (sess, TRUE); } else if (!wocky_strdiff (elt, "unhold")) { set_hold (sess, FALSE); } /* XEP-0178 says that only and can have a name='' * attribute. */ else if (!wocky_strdiff (elt, "mute")) { return set_mute (sess, name, creator, TRUE, error); } else if (!wocky_strdiff (elt, "unmute")) { return set_mute (sess, name, creator, FALSE, error); } else { g_set_error (error, WOCKY_JINGLE_ERROR, WOCKY_JINGLE_ERROR_UNSUPPORTED_INFO, "<%s> is not known in namespace %s", elt, ns); return FALSE; } return TRUE; } static void on_session_info (WockyJingleSession *sess, WockyNode *node, GError **error) { gboolean understood_a_payload = FALSE; gboolean hit_an_error = FALSE; WockyNodeIter i; WockyNode *n; /* if this is a ping, just ack it. */ if (wocky_node_get_first_child (node) == NULL) return; wocky_node_iter_init (&i, node, NULL, NULL); while (wocky_node_iter_next (&i, &n)) { gboolean handled; GError *e = NULL; if (handle_payload (sess, n, &handled, &e)) { understood_a_payload = understood_a_payload || handled; } else if (hit_an_error) { DEBUG ("already got another error; ignoring %s", e->message); g_error_free (e); } else { DEBUG ("hit an error: %s", e->message); hit_an_error = TRUE; g_propagate_error (error, e); } } /* If we understood something, the remote state (may have) changed. Else, * return an error to the peer. */ if (understood_a_payload) g_signal_emit (sess, signals[REMOTE_STATE_CHANGED], 0); else if (!hit_an_error) g_set_error (error, WOCKY_JINGLE_ERROR, WOCKY_JINGLE_ERROR_UNSUPPORTED_INFO, "no recognized session-info payloads"); } static void on_session_terminate (WockyJingleSession *sess, WockyNode *node, GError **error) { gchar *text = NULL; WockyNode *n = wocky_node_get_child (node, "reason"); WockyJingleReason wocky_jingle_reason = WOCKY_JINGLE_REASON_UNKNOWN; if (n != NULL) extract_reason (n, &wocky_jingle_reason, &text); DEBUG ("remote end terminated the session with reason %s and text '%s'", wocky_jingle_session_get_reason_name (wocky_jingle_reason), (text != NULL ? text : "(none)")); set_state (sess, WOCKY_JINGLE_STATE_ENDED, wocky_jingle_reason, text); g_free (text); } static void on_transport_info (WockyJingleSession *sess, WockyNode *node, GError **error) { WockyJingleSessionPrivate *priv = sess->priv; WockyJingleContent *c = NULL; if (WOCKY_JINGLE_DIALECT_IS_GOOGLE (priv->dialect)) { GHashTableIter iter; gpointer value; if (priv->dialect == WOCKY_JINGLE_DIALECT_GTALK4) { if (!wocky_strdiff (wocky_node_get_attribute (node, "type"), "candidates")) { GList *contents = wocky_jingle_session_get_contents (sess); GList *l; DEBUG ("switching to gtalk3 dialect and retransmiting our candidates"); priv->dialect = WOCKY_JINGLE_DIALECT_GTALK3; for (l = contents; l != NULL; l = l->next) wocky_jingle_content_retransmit_candidates (l->data, TRUE); g_list_free (contents); } else { node = wocky_node_get_child (node, "transport"); if (node == NULL) { g_set_error (error, WOCKY_XMPP_ERROR, WOCKY_XMPP_ERROR_BAD_REQUEST, "transport-info stanza without a "); return; } } } g_hash_table_iter_init (&iter, priv->initiator_contents); while (g_hash_table_iter_next (&iter, NULL, &value)) { c = value; wocky_jingle_content_parse_transport_info (c, node, error); if (error != NULL && *error != NULL) break; } } else { WockyNodeIter i; WockyNode *content_node; GError *e = NULL; wocky_node_iter_init (&i, node, "content", NULL); while (wocky_node_iter_next (&i, &content_node)) { WockyNode *transport_node; if (lookup_content (sess, wocky_node_get_attribute (content_node, "name"), wocky_node_get_attribute (content_node, "creator"), TRUE /* fail_if_missing */, &c, &e)) { /* we need transport child of content node */ transport_node = wocky_node_get_child ( content_node, "transport"); wocky_jingle_content_parse_transport_info (c, transport_node, &e); } /* Save the first error we encounter, but go through all remaining * contents anyway to try and recover as much info as we can */ if (e != NULL && error != NULL && *error == NULL) { *error = e; e = NULL; } g_clear_error (&e); } } } static void on_transport_accept (WockyJingleSession *sess, WockyNode *node, GError **error) { DEBUG ("Ignoring 'transport-accept' action from peer"); } static void on_description_info (WockyJingleSession *sess, WockyNode *node, GError **error) { _foreach_content (sess, node, TRUE, _each_description_info, NULL, error); } static void on_info (WockyJingleSession *sess, WockyNode *node, GError **error) { WockyJingleSessionPrivate *priv = sess->priv; WockyJingleContent *c = NULL; DEBUG ("received info "); if (WOCKY_JINGLE_DIALECT_IS_GOOGLE (priv->dialect)) { GHashTableIter iter; g_hash_table_iter_init (&iter, priv->initiator_contents); while (g_hash_table_iter_next (&iter, NULL, (gpointer) &c)) { wocky_jingle_content_parse_info (c, node, error); if (error != NULL && *error != NULL) break; } } } static HandlerFunc handlers[] = { NULL, /* for unknown action */ on_content_accept, on_content_add, on_content_modify, on_content_remove, on_content_replace, on_content_reject, on_session_accept, /* jingle_on_session_accept */ on_session_info, on_session_initiate, on_session_terminate, /* jingle_on_session_terminate */ on_transport_info, /* jingle_on_transport_info */ on_transport_accept, on_description_info, on_info }; static gboolean wocky_jingle_state_machine_dance (WockyJingleSession *sess, WockyJingleAction action, WockyNode *node, GError **error) { WockyJingleSessionPrivate *priv = sess->priv; GError *e = NULL; /* parser should've checked this already */ g_assert (action_is_allowed (action, priv->state)); g_assert (handlers[action] != NULL); handlers[action] (sess, node, &e); if (e != NULL) { g_propagate_error (error, e); return FALSE; } return TRUE; } static WockyJingleDialect detect_google_dialect (WockyNode *session_node) { /* The GTALK3 dialect is the only one that supports video at this time */ if (wocky_node_get_child_ns (session_node, "description", WOCKY_XMPP_NS_GOOGLE_SESSION_VIDEO) != NULL) return WOCKY_JINGLE_DIALECT_GTALK3; /* GTalk4 has a transport item, GTalk3 doesn't */ if (wocky_node_get_child_ns (session_node, "transport", WOCKY_XMPP_NS_GOOGLE_TRANSPORT_P2P) == NULL) return WOCKY_JINGLE_DIALECT_GTALK3; return WOCKY_JINGLE_DIALECT_GTALK4; } static const gchar * wocky_jingle_session_detect_internal ( WockyStanza *stanza, WockyJingleAction *action, WockyJingleDialect *dialect, WockyNode **session_node_out) { const gchar *actxt, *sid; WockyNode *iq_node, *session_node; WockyStanzaSubType sub_type; gboolean google_mode = FALSE; /* all jingle actions are sets */ wocky_stanza_get_type_info (stanza, NULL, &sub_type); if (sub_type != WOCKY_STANZA_SUB_TYPE_SET) return NULL; iq_node = wocky_stanza_get_top_node (stanza); if ((NULL == wocky_stanza_get_from (stanza)) || (NULL == wocky_stanza_get_to (stanza))) return NULL; /* first, we try standard jingle */ session_node = wocky_node_get_child_ns (iq_node, "jingle", WOCKY_XMPP_NS_JINGLE); if (session_node != NULL) { if (dialect != NULL) *dialect = WOCKY_JINGLE_DIALECT_V032; } else { /* then, we try a bit older jingle version */ session_node = wocky_node_get_child_ns (iq_node, "jingle", WOCKY_XMPP_NS_JINGLE015); if (session_node != NULL) { if (dialect != NULL) *dialect = WOCKY_JINGLE_DIALECT_V015; } else { /* next, we try googletalk */ session_node = wocky_node_get_child_ns (iq_node, "session", WOCKY_XMPP_NS_GOOGLE_SESSION); if (session_node != NULL) { if (dialect != NULL) *dialect = detect_google_dialect (session_node); google_mode = TRUE; } else { return NULL; } } } if (google_mode) { actxt = wocky_node_get_attribute (session_node, "type"); sid = wocky_node_get_attribute (session_node, "id"); } else { actxt = wocky_node_get_attribute (session_node, "action"); sid = wocky_node_get_attribute (session_node, "sid"); } if (session_node_out != NULL) *session_node_out = session_node; if (action != NULL) *action = parse_action (actxt); return sid; } const gchar * wocky_jingle_session_detect ( WockyStanza *stanza, WockyJingleAction *action, WockyJingleDialect *dialect) { g_return_val_if_fail (WOCKY_IS_STANZA (stanza), NULL); return wocky_jingle_session_detect_internal (stanza, action, dialect, NULL); } gboolean wocky_jingle_session_parse ( WockyJingleSession *sess, WockyJingleAction action, WockyStanza *stanza, GError **error) { WockyJingleSessionPrivate *priv; WockyNode *iq_node, *session_node; const gchar *from, *action_name; g_return_val_if_fail (WOCKY_IS_JINGLE_SESSION (sess), FALSE); g_return_val_if_fail (WOCKY_IS_STANZA (stanza), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); priv = sess->priv; /* IQ from/to can come in handy */ from = wocky_stanza_get_from (stanza); iq_node = wocky_stanza_get_top_node (stanza); if (action == WOCKY_JINGLE_ACTION_UNKNOWN) { g_set_error (error, WOCKY_XMPP_ERROR, WOCKY_XMPP_ERROR_BAD_REQUEST, "unknown session action"); return FALSE; } action_name = produce_action (action, priv->dialect); DEBUG ("jingle action '%s' from '%s' in session '%s' dialect %u state %u", action_name, from, priv->sid, priv->dialect, priv->state); switch (priv->dialect) { case WOCKY_JINGLE_DIALECT_V032: session_node = wocky_node_get_child_ns (iq_node, "jingle", WOCKY_XMPP_NS_JINGLE); break; case WOCKY_JINGLE_DIALECT_V015: session_node = wocky_node_get_child_ns (iq_node, "jingle", WOCKY_XMPP_NS_JINGLE015); break; case WOCKY_JINGLE_DIALECT_GTALK3: case WOCKY_JINGLE_DIALECT_GTALK4: session_node = wocky_node_get_child_ns (iq_node, "session", WOCKY_XMPP_NS_GOOGLE_SESSION); break; default: /* just to make gcc happy about dealing with default case */ session_node = NULL; } if (session_node == NULL) { g_set_error (error, WOCKY_XMPP_ERROR, WOCKY_XMPP_ERROR_BAD_REQUEST, "malformed jingle stanza"); return FALSE; } if (!wocky_jingle_session_defines_action (sess, action)) { g_set_error (error, WOCKY_XMPP_ERROR, WOCKY_XMPP_ERROR_BAD_REQUEST, "action '%s' unknown (using dialect %u)", action_name, priv->dialect); return FALSE; } if (!action_is_allowed (action, priv->state)) { g_set_error (error, WOCKY_JINGLE_ERROR, WOCKY_JINGLE_ERROR_OUT_OF_ORDER, "action '%s' not allowed in current state", action_name); return FALSE; } return wocky_jingle_state_machine_dance (sess, action, session_node, error); } WockyStanza * wocky_jingle_session_new_message (WockyJingleSession *sess, WockyJingleAction action, WockyNode **sess_node) { WockyJingleSessionPrivate *priv = sess->priv; WockyStanza *stanza; WockyNode *session_node; gchar *el = NULL, *ns = NULL; gboolean gtalk_mode = FALSE; g_return_val_if_fail (WOCKY_IS_JINGLE_SESSION (sess), NULL); g_return_val_if_fail (action != WOCKY_JINGLE_ACTION_UNKNOWN, NULL); g_assert ((action == WOCKY_JINGLE_ACTION_SESSION_INITIATE) || (priv->state > WOCKY_JINGLE_STATE_PENDING_CREATED)); switch (priv->dialect) { case WOCKY_JINGLE_DIALECT_V032: el = "jingle"; ns = WOCKY_XMPP_NS_JINGLE; break; case WOCKY_JINGLE_DIALECT_V015: el = "jingle"; ns = WOCKY_XMPP_NS_JINGLE015; break; case WOCKY_JINGLE_DIALECT_GTALK3: case WOCKY_JINGLE_DIALECT_GTALK4: el = "session"; ns = WOCKY_XMPP_NS_GOOGLE_SESSION; gtalk_mode = TRUE; break; case WOCKY_JINGLE_DIALECT_ERROR: g_assert_not_reached (); } stanza = wocky_stanza_build ( WOCKY_STANZA_TYPE_IQ, WOCKY_STANZA_SUB_TYPE_SET, NULL, priv->peer_jid, '(', el, ':', ns, '*', &session_node, ')', NULL); wocky_node_set_attributes (session_node, "initiator", priv->initiator, (gtalk_mode) ? "id" : "sid", priv->sid, (gtalk_mode) ? "type" : "action", produce_action (action, priv->dialect), NULL); if (sess_node != NULL) *sess_node = session_node; return stanza; } typedef void (*ContentMapperFunc) (WockyJingleSession *sess, WockyJingleContent *c, gpointer user_data); static void _map_initial_contents (WockyJingleSession *sess, ContentMapperFunc mapper, gpointer user_data) { GList *li; GList *contents = wocky_jingle_session_get_contents (sess); for (li = contents; li; li = li->next) { WockyJingleContent *c = WOCKY_JINGLE_CONTENT (li->data); const gchar *disposition = wocky_jingle_content_get_disposition (c); if (!wocky_strdiff (disposition, "session")) mapper (sess, c, user_data); } g_list_free (contents); } static void _check_content_ready (WockyJingleSession *sess, WockyJingleContent *c, gpointer user_data) { gboolean *ready = (gboolean *) user_data; if (!wocky_jingle_content_is_ready (c)) { *ready = FALSE; } } static void _transmit_candidates (WockyJingleSession *sess, WockyJingleContent *c, gpointer user_data) { wocky_jingle_content_retransmit_candidates (c, FALSE); } static void _fill_content (WockyJingleSession *sess, WockyJingleContent *c, gpointer user_data) { WockyNode *sess_node = user_data; WockyNode *transport_node; WockyJingleContentState state; wocky_jingle_content_produce_node (c, sess_node, TRUE, TRUE, &transport_node); wocky_jingle_content_inject_candidates (c, transport_node); g_object_get (c, "state", &state, NULL); if (state == WOCKY_JINGLE_CONTENT_STATE_EMPTY) { g_object_set (c, "state", WOCKY_JINGLE_CONTENT_STATE_SENT, NULL); } else if (state == WOCKY_JINGLE_CONTENT_STATE_NEW) { g_object_set (c, "state", WOCKY_JINGLE_CONTENT_STATE_ACKNOWLEDGED, NULL); } else { DEBUG ("content %p is in state %u", c, state); g_assert_not_reached (); } } /** * wocky_jingle_session_send: * @sess: a session * @stanza: (transfer full): a stanza, of which this function will take ownership * * A shorthand for sending a Jingle IQ without waiting for the reply. */ void wocky_jingle_session_send (WockyJingleSession *sess, WockyStanza *stanza) { g_return_if_fail (WOCKY_IS_JINGLE_SESSION (sess)); g_return_if_fail (WOCKY_IS_STANZA (stanza)); wocky_porter_send_iq_async (sess->priv->porter, stanza, NULL, NULL, NULL); g_object_unref (stanza); } static void _on_initiate_reply ( GObject *source, GAsyncResult *result, gpointer user_data) { WockyPorter *porter = WOCKY_PORTER (source); WockyJingleSession *sess = WOCKY_JINGLE_SESSION (user_data); WockyJingleSessionPrivate *priv = sess->priv; WockyStanza *reply; if (priv->state != WOCKY_JINGLE_STATE_PENDING_INITIATE_SENT) { DEBUG ("Ignoring session-initiate reply; session %p is in state %u.", sess, priv->state); g_object_unref (sess); return; } reply = wocky_porter_send_iq_finish (porter, result, NULL); if (reply != NULL && !wocky_stanza_extract_errors (reply, NULL, NULL, NULL, NULL)) { set_state (sess, WOCKY_JINGLE_STATE_PENDING_INITIATED, 0, NULL); if (priv->dialect != WOCKY_JINGLE_DIALECT_V032) { /* If this is a dialect that doesn't support , we treat the * session-initiate being acked as the cue to say we're ringing. */ priv->remote_ringing = TRUE; g_signal_emit (sess, signals[REMOTE_STATE_CHANGED], 0); } } else { set_state (sess, WOCKY_JINGLE_STATE_ENDED, WOCKY_JINGLE_REASON_UNKNOWN, NULL); } g_clear_object (&reply); g_object_unref (sess); } static void _on_accept_reply ( GObject *source, GAsyncResult *result, gpointer user_data) { WockyPorter *porter = WOCKY_PORTER (source); WockyJingleSession *sess = WOCKY_JINGLE_SESSION (user_data); WockyJingleSessionPrivate *priv = sess->priv; WockyStanza *reply; if (priv->state != WOCKY_JINGLE_STATE_PENDING_ACCEPT_SENT) { DEBUG ("Ignoring session-accept reply; session %p is in state %u.", sess, priv->state); g_object_unref (sess); return; } reply = wocky_porter_send_iq_finish (porter, result, NULL); if (reply != NULL && !wocky_stanza_extract_errors (reply, NULL, NULL, NULL, NULL)) { set_state (sess, WOCKY_JINGLE_STATE_ACTIVE, 0, NULL); wocky_jingle_session_send_rtp_info (sess, "active"); } else { set_state (sess, WOCKY_JINGLE_STATE_ENDED, WOCKY_JINGLE_REASON_UNKNOWN, NULL); } g_clear_object (&reply); g_object_unref (sess); } static void try_session_initiate_or_accept (WockyJingleSession *sess) { WockyJingleSessionPrivate *priv = sess->priv; WockyStanza *msg; WockyNode *sess_node; gboolean contents_ready = TRUE; WockyJingleAction action; WockyJingleState new_state; GAsyncReadyCallback handler; DEBUG ("Trying initiate or accept"); /* If there are no contents yet, we shouldn't have been called at all. */ g_assert (g_hash_table_size (priv->initiator_contents) + g_hash_table_size (priv->responder_contents) > 0); if (priv->local_initiator) { if (priv->state != WOCKY_JINGLE_STATE_PENDING_CREATED) { DEBUG ("session is in state %u, won't try to initiate", priv->state); return; } if (!priv->locally_accepted) { DEBUG ("session not locally accepted yet, not initiating"); return; } action = WOCKY_JINGLE_ACTION_SESSION_INITIATE; new_state = WOCKY_JINGLE_STATE_PENDING_INITIATE_SENT; handler = _on_initiate_reply; } else { if (priv->state != WOCKY_JINGLE_STATE_PENDING_INITIATED) { DEBUG ("session is in state %u, won't try to accept", priv->state); return; } if (!priv->locally_accepted) { DEBUG ("session not locally accepted yet, not accepting"); return; } action = WOCKY_JINGLE_ACTION_SESSION_ACCEPT; new_state = WOCKY_JINGLE_STATE_PENDING_ACCEPT_SENT; handler = _on_accept_reply; } _map_initial_contents (sess, _check_content_ready, &contents_ready); DEBUG ("Contents are ready: %s", contents_ready ? "yes" : "no"); if (!contents_ready) { DEBUG ("Contents not yet ready, not initiating/accepting now.."); return; } if (action == WOCKY_JINGLE_ACTION_SESSION_INITIATE) g_signal_emit (sess, signals[ABOUT_TO_INITIATE], 0); msg = wocky_jingle_session_new_message (sess, action, &sess_node); if (priv->dialect == WOCKY_JINGLE_DIALECT_GTALK3) { gboolean has_video = FALSE; gboolean has_audio = FALSE; GHashTableIter iter; gpointer value; g_hash_table_iter_init (&iter, priv->initiator_contents); while (g_hash_table_iter_next (&iter, NULL, &value)) { WockyJingleMediaType type; g_object_get (value, "media-type", &type, NULL); if (type == WOCKY_JINGLE_MEDIA_TYPE_VIDEO) { has_video = TRUE; } else if (type == WOCKY_JINGLE_MEDIA_TYPE_AUDIO) { has_audio = TRUE; } } if (has_video || has_audio) { sess_node = wocky_node_add_child_ns_q (sess_node, "description", g_quark_from_static_string (has_video ? WOCKY_XMPP_NS_GOOGLE_SESSION_VIDEO : WOCKY_XMPP_NS_GOOGLE_SESSION_PHONE)); } } _map_initial_contents (sess, _fill_content, sess_node); wocky_porter_send_iq_async (priv->porter, msg, NULL, handler, g_object_ref (sess)); g_object_unref (msg); set_state (sess, new_state, 0, NULL); /* now all initial contents can transmit their candidates */ _map_initial_contents (sess, _transmit_candidates, NULL); } /** * set_state: * @sess: a jingle session * @state: the new state for the session * @termination_reason: if @state is WOCKY_JINGLE_STATE_ENDED, the reason the session * ended. Otherwise, must be WOCKY_JINGLE_REASON_UNKNOWN. * @text: if @state is WOCKY_JINGLE_STATE_ENDED, the human-readable reason the session * ended. */ static void set_state (WockyJingleSession *sess, WockyJingleState state, WockyJingleReason termination_reason, const gchar *text) { WockyJingleSessionPrivate *priv = sess->priv; if (state <= priv->state) { DEBUG ("ignoring request to set state from %u back to %u", priv->state, state); return; } if (state != WOCKY_JINGLE_STATE_ENDED) g_assert (termination_reason == WOCKY_JINGLE_REASON_UNKNOWN); DEBUG ("Setting state of JingleSession: %p (priv = %p) from %u to %u", sess, priv, priv->state, state); priv->state = state; g_object_notify (G_OBJECT (sess), "state"); /* If we have an outstanding "you're on hold notification", send it */ if (priv->local_hold && state >= WOCKY_JINGLE_STATE_PENDING_INITIATED && state < WOCKY_JINGLE_STATE_ENDED) wocky_jingle_session_send_held (sess); if (state == WOCKY_JINGLE_STATE_ENDED) g_signal_emit (sess, signals[TERMINATED], 0, priv->locally_terminated, termination_reason, text); } /** * wocky_jingle_session_accept: * @sess: the session. * * For incoming calls, accepts the call. For outgoing calls, indicates that the * initial contents for the call have been created and the offer can be sent to * the peer. * * The acceptance or offer will only be signalled to the peer once all contents * are ready (as returned by wocky_jingle_content_is_ready()). For an RTP * session with #WockyJingleMediaRtp contents, this translates to a media * description and transport candidates having been provided to all contents. */ void wocky_jingle_session_accept (WockyJingleSession *sess) { g_return_if_fail (WOCKY_IS_JINGLE_SESSION (sess)); sess->priv->locally_accepted = TRUE; try_session_initiate_or_accept (sess); } const gchar * wocky_jingle_session_get_reason_name (WockyJingleReason reason) { GEnumClass *klass = g_type_class_ref (wocky_jingle_reason_get_type ()); GEnumValue *enum_value = g_enum_get_value (klass, (gint) reason); g_return_val_if_fail (enum_value != NULL, NULL); return enum_value->value_nick; } /** * wocky_jingle_session_terminate: * @sess: the session * @reason: the reason the session should be terminated * @text: (allow-none): human-readable information about why the session * terminated * @error: Unused, because this function never fails. * * Ends a session. * * If called for an outgoing session which has not yet been signalled to the * peer (perhaps because wocky_jingle_session_accept() has not been called, or * codecs or candidates have not been provided), the session will quietly * terminate without the peer hearing anything about it. * * If called for an already-terminated session, this is a no-op. * * Returns: %TRUE. */ gboolean wocky_jingle_session_terminate (WockyJingleSession *sess, WockyJingleReason reason, const gchar *text, GError **error G_GNUC_UNUSED) { WockyJingleSessionPrivate *priv; const gchar *reason_elt; g_return_val_if_fail (WOCKY_IS_JINGLE_SESSION (sess), FALSE); priv = sess->priv; if (priv->state == WOCKY_JINGLE_STATE_ENDED) { DEBUG ("session already terminated, ignoring terminate request"); return TRUE; } if (reason == WOCKY_JINGLE_REASON_UNKNOWN) reason = (priv->state == WOCKY_JINGLE_STATE_ACTIVE) ? WOCKY_JINGLE_REASON_SUCCESS : WOCKY_JINGLE_REASON_CANCEL; reason_elt = wocky_jingle_session_get_reason_name (reason); if (priv->state != WOCKY_JINGLE_STATE_PENDING_CREATED) { WockyNode *session_node; WockyStanza *msg = wocky_jingle_session_new_message (sess, WOCKY_JINGLE_ACTION_SESSION_TERMINATE, &session_node); if (priv->dialect == WOCKY_JINGLE_DIALECT_V032 && reason_elt != NULL) { WockyNode *r = wocky_node_add_child_with_content (session_node, "reason", NULL); wocky_node_add_child (r, reason_elt); if (text != NULL && *text != '\0') wocky_node_add_child_with_content (r, "text", text); } wocky_jingle_session_send (sess, msg); } /* NOTE: on "terminated", jingle factory and media channel will unref * it, bringing refcount to 0, so dispose will be called, and it * takes care of cleanup */ DEBUG ("we are terminating this session"); priv->locally_terminated = TRUE; set_state (sess, WOCKY_JINGLE_STATE_ENDED, reason, text); return TRUE; } static void _foreach_count_active_contents (gpointer key, gpointer value, gpointer user_data) { WockyJingleContent *c = value; guint *n_contents = user_data; WockyJingleContentState state; g_object_get (c, "state", &state, NULL); if ((state >= WOCKY_JINGLE_CONTENT_STATE_NEW) && (state < WOCKY_JINGLE_CONTENT_STATE_REMOVING)) { *n_contents = *n_contents + 1; } } static gboolean count_active_contents (WockyJingleSession *sess) { WockyJingleSessionPrivate *priv = sess->priv; guint n_contents = 0; g_hash_table_foreach (priv->initiator_contents, _foreach_count_active_contents, &n_contents); g_hash_table_foreach (priv->responder_contents, _foreach_count_active_contents, &n_contents); return n_contents; } static void content_removed_cb (WockyJingleContent *c, gpointer user_data) { WockyJingleSession *sess = WOCKY_JINGLE_SESSION (user_data); WockyJingleSessionPrivate *priv = sess->priv; const gchar *name = wocky_jingle_content_get_name (c); if (wocky_jingle_content_creator_is_initiator (c)) g_hash_table_remove (priv->initiator_contents, name); else g_hash_table_remove (priv->responder_contents, name); if (priv->state == WOCKY_JINGLE_STATE_ENDED) return; if (count_active_contents (sess) == 0) { wocky_jingle_session_terminate (sess, WOCKY_JINGLE_REASON_UNKNOWN, NULL, NULL); } else { /* It's possible the content now removed was * blocking us from creating or accepting the * session, so we might as well try now. */ try_session_initiate_or_accept (sess); } } void wocky_jingle_session_remove_content (WockyJingleSession *sess, WockyJingleContent *c) { g_return_if_fail (WOCKY_IS_JINGLE_SESSION (sess)); g_return_if_fail (WOCKY_IS_JINGLE_CONTENT (c)); if (count_active_contents (sess) > 1) { wocky_jingle_content_remove (c, TRUE); } else { /* session will be terminated when the content gets marked as removed */ DEBUG ("called for last active content, doing session-terminate instead"); wocky_jingle_content_remove (c, FALSE); } } /** * wocky_jingle_session_add_content: * @sess: the session * @mtype: what kind of media will be exchanged on the content * @senders: which directions media should initially flow in. * @name: (allow-none): a descriptive name to use for the content; this is * typically not shown to users * @content_ns: the namespace to use for the content's description * @transport_ns: the namespace of the media transport to use for the call * * Adds a content to the session. Once it has its codecs and transport * candidates filled in, it will be signalled to the peer (either as part of * the session-initiate, if it has not been sent yet, or as a content-add if * @sess has already been initiated). * * Legal values for @content_ns and @transport_ns depend on the Jingle dialect * in use for this session (and in some cases on @mtype); sensible values * depend on the peer's capabilities. * * Returns: (transfer none): the new content, which is guaranteed not to be %NULL. */ WockyJingleContent * wocky_jingle_session_add_content (WockyJingleSession *sess, WockyJingleMediaType mtype, WockyJingleContentSenders senders, const gchar *name, const gchar *content_ns, const gchar *transport_ns) { WockyJingleSessionPrivate *priv; WockyJingleContent *c; GType content_type; GHashTable *contents; guint id; gchar *cname = NULL; g_return_val_if_fail (WOCKY_IS_JINGLE_SESSION (sess), NULL); priv = sess->priv; contents = priv->local_initiator ? priv->initiator_contents : priv->responder_contents; id = g_hash_table_size (contents) + 1; if (name == NULL || *name == '\0') name = (mtype == WOCKY_JINGLE_MEDIA_TYPE_AUDIO ? "Audio" : "Video"); cname = g_strdup (name); while (g_hash_table_lookup (priv->initiator_contents, cname) != NULL || g_hash_table_lookup (priv->responder_contents, cname) != NULL) { g_free (cname); cname = g_strdup_printf ("%s_%d", name, id++); } content_type = wocky_jingle_factory_lookup_content_type ( wocky_jingle_session_get_factory (sess), content_ns); g_assert (content_type != 0); c = create_content (sess, content_type, mtype, senders, content_ns, transport_ns, cname, NULL, NULL); /* The new content better have ended up in the set we thought it would... */ g_assert (g_hash_table_lookup (contents, cname) != NULL); g_free (cname); return c; } /* Get any content. Either we're in google mode (so we only have one content * anyways), or we just need any content type to figure out what use case * we're in (media, ft, etc). */ static WockyJingleContent * _get_any_content (WockyJingleSession *session) { WockyJingleContent *c; GList *li = wocky_jingle_session_get_contents (session); if (li == NULL) return NULL; c = li->data; g_list_free (li); return c; } /* Note: if there are multiple content types, not guaranteed which one will * be returned. Typically, the same GType will know how to handle related * contents found in a session (e.g. media-rtp for audio/video), so that * should not be a problem. Returns 0 if there are no contents yet. */ GType wocky_jingle_session_get_content_type (WockyJingleSession *sess) { WockyJingleContent *c; g_return_val_if_fail (WOCKY_IS_JINGLE_SESSION (sess), G_TYPE_INVALID); c = _get_any_content (sess); if (c == NULL) return 0; return G_OBJECT_TYPE (c); } /* FIXME: probably should make this into a property */ GList * wocky_jingle_session_get_contents (WockyJingleSession *sess) { WockyJingleSessionPrivate *priv; g_return_val_if_fail (WOCKY_IS_JINGLE_SESSION (sess), NULL); priv = sess->priv; return g_list_concat (g_hash_table_get_values (priv->initiator_contents), g_hash_table_get_values (priv->responder_contents)); } const gchar * wocky_jingle_session_get_peer_resource (WockyJingleSession *sess) { g_return_val_if_fail (WOCKY_IS_JINGLE_SESSION (sess), NULL); return sess->priv->peer_resource; } const gchar * wocky_jingle_session_get_initiator (WockyJingleSession *sess) { g_return_val_if_fail (WOCKY_IS_JINGLE_SESSION (sess), NULL); return sess->priv->initiator; } const gchar * wocky_jingle_session_get_sid (WockyJingleSession *sess) { g_return_val_if_fail (WOCKY_IS_JINGLE_SESSION (sess), NULL); return sess->priv->sid; } static void content_ready_cb (WockyJingleContent *c, gpointer user_data) { WockyJingleSession *sess = WOCKY_JINGLE_SESSION (user_data); const gchar *disposition; DEBUG ("called"); disposition = wocky_jingle_content_get_disposition (c); /* This assertion is actually safe, because 'ready' is only emitted by * contents with disposition "session". But this is crazy. */ g_assert (!wocky_strdiff (disposition, "session")); try_session_initiate_or_accept (sess); } static void wocky_jingle_session_send_rtp_info (WockyJingleSession *sess, const gchar *name) { WockyStanza *message; WockyNode *jingle; if (!wocky_jingle_session_defines_action (sess, WOCKY_JINGLE_ACTION_SESSION_INFO)) { DEBUG ("Not sending <%s/>; not using modern Jingle", name); return; } message = wocky_jingle_session_new_message (sess, WOCKY_JINGLE_ACTION_SESSION_INFO, &jingle); wocky_node_add_child_ns_q (jingle, name, g_quark_from_static_string (WOCKY_XMPP_NS_JINGLE_RTP_INFO)); /* This is just informational, so ignoring the reply. */ wocky_jingle_session_send (sess, message); } static void wocky_jingle_session_send_held (WockyJingleSession *sess) { const gchar *s = (sess->priv->local_hold ? "hold" : "unhold"); wocky_jingle_session_send_rtp_info (sess, s); } void wocky_jingle_session_set_local_hold (WockyJingleSession *sess, gboolean held) { g_return_if_fail (WOCKY_IS_JINGLE_SESSION (sess)); g_object_set (sess, "local-hold", held, NULL); } gboolean wocky_jingle_session_get_remote_hold (WockyJingleSession *sess) { g_return_val_if_fail (WOCKY_IS_JINGLE_SESSION (sess), FALSE); return sess->priv->remote_hold; } gboolean wocky_jingle_session_get_remote_ringing (WockyJingleSession *sess) { g_return_val_if_fail (WOCKY_IS_JINGLE_SESSION (sess), FALSE); return sess->priv->remote_ringing; } gboolean wocky_jingle_session_can_modify_contents (WockyJingleSession *sess) { g_return_val_if_fail (WOCKY_IS_JINGLE_SESSION (sess), FALSE); return !WOCKY_JINGLE_DIALECT_IS_GOOGLE (sess->priv->dialect) && !wocky_jingle_session_peer_has_cap (sess, WOCKY_QUIRK_GOOGLE_WEBMAIL_CLIENT); } WockyJingleDialect wocky_jingle_session_get_dialect (WockyJingleSession *sess) { g_return_val_if_fail (WOCKY_IS_JINGLE_SESSION (sess), WOCKY_JINGLE_DIALECT_ERROR); return sess->priv->dialect; } WockyContact * wocky_jingle_session_get_peer_contact (WockyJingleSession *self) { g_return_val_if_fail (WOCKY_IS_JINGLE_SESSION (self), NULL); return self->priv->peer_contact; } /* * wocky_jingle_session_get_peer_jid: * @sess: a jingle session * * Returns: the full JID of the remote contact. */ const gchar * wocky_jingle_session_get_peer_jid (WockyJingleSession *sess) { g_return_val_if_fail (WOCKY_IS_JINGLE_SESSION (sess), NULL); return sess->priv->peer_jid; } WockyJingleFactory * wocky_jingle_session_get_factory (WockyJingleSession *self) { g_return_val_if_fail (WOCKY_IS_JINGLE_SESSION (self), NULL); return self->priv->jingle_factory; } WockyPorter * wocky_jingle_session_get_porter (WockyJingleSession *self) { g_return_val_if_fail (WOCKY_IS_JINGLE_SESSION (self), NULL); return self->priv->porter; } void wocky_jingle_session_acknowledge_iq (WockyJingleSession *self, WockyStanza *stanza) { g_return_if_fail (WOCKY_IS_JINGLE_SESSION (self)); g_return_if_fail (WOCKY_IS_STANZA (stanza)); if (wocky_jingle_session_peer_has_cap (self, WOCKY_QUIRK_GOOGLE_WEBMAIL_CLIENT)) { WockyJingleAction action = WOCKY_JINGLE_ACTION_UNKNOWN; WockyNode *used_node = NULL; /* As of 2013-05-29, the Google webmail client sends a session-initiate * IQ with two child nodes (which is not valid XMPP Core but never mind) * and replies to session-initiate by echoing the child for the dialect * it chose. We have to do the same echoing, otherwise it can't call us. * * It doesn't seem to reply to our other IQs at all; we still reply * here (we'd be violating XMPP Core if we didn't), but we don't * bother putting content in the IQ, to reduce bandwidth. */ if (wocky_jingle_session_detect_internal (stanza, &action, NULL, &used_node) != NULL && action == WOCKY_JINGLE_ACTION_SESSION_INITIATE) { WockyStanza *reply = wocky_stanza_build_iq_result (stanza, NULL); if (reply != NULL) { WockyNode *reply_node = wocky_stanza_get_top_node (reply); reply_node->children = g_slist_append (reply_node->children, _wocky_node_copy (used_node)); wocky_porter_send (self->priv->porter, reply); g_object_unref (reply); return; } } } /* normal Jingle just says "OK" without echoing */ wocky_porter_acknowledge_iq (self->priv->porter, stanza, NULL); }