diff options
author | David Laban <david.laban@collabora.co.uk> | 2011-02-12 10:03:25 +0000 |
---|---|---|
committer | David Laban <david.laban@collabora.co.uk> | 2011-02-12 11:02:57 +0000 |
commit | 8dcddb61752cfa140df30c94b54bd02684419323 (patch) | |
tree | 5ba95bbfdafdfd8152d783c180bfd8c49499fd7e /rakia | |
parent | ada52bcb3064a5a7fa0e75038f4365b45376518c (diff) |
Rename project to telepathy-rakia
For the curious, these are the commands you need:
git clean -d
git mv tpsip-extensions extensions
git mv tpsip rakia
sed -i \
-e 's/libtpsip-extensions.la/librakia-extensions.la/g' \
-e 's/libtpsip_extensions_la/librakia_extensions_la/g' \
-e 's/tpsip-extensions/extensions/g' \
-e 's/sofiasip/rakia/g' \
-e 's/tpsip/rakia/g' \
-e 's/Tpsip/Rakia/g' \
-e 's/SOFIASIP/RAKIA/g' \
*/*.[ch] .gitignore configure.ac $(find -name 'Makefile.am' ;
find -name '*.py' ; find -name '*.in')
git mv ./docs/telepathy-sofiasip.8.in ./docs/telepathy-rakia.8.in
git mv ./src/telepathy-sofiasip.c ./src/telepathy-rakia.c
git mv ./data/org.freedesktop.Telepathy.ConnectionManager.sofiasip.service.in \
./data/org.freedesktop.Telepathy.ConnectionManager.rakia.service.in
git mv ./tests/twisted/tools/sofiasip.service.in \
./tests/twisted/tools/rakia.service.in
./autogen.sh && make distcheck
Diffstat (limited to 'rakia')
30 files changed, 11831 insertions, 0 deletions
diff --git a/rakia/Makefile.am b/rakia/Makefile.am new file mode 100644 index 0000000..3e8f03d --- /dev/null +++ b/rakia/Makefile.am @@ -0,0 +1,68 @@ +# +# Makefile.am for telepathy-rakia/rakia +# +# Copyright (C) 2008 Nokia Corporation +# Contact: Mikhail Zabaluev <mikhail.zabaluev@nokia.com> +# Licensed under LGPL. See file COPYING. +# + +noinst_LTLIBRARIES = librakia.la + +AM_CPPFLAGS = @GLIB_CFLAGS@ @TELEPATHY_GLIB_CFLAGS@ @SOFIA_SIP_UA_CFLAGS@ \ + -I$(top_builddir) -I$(top_srcdir) + +AM_CFLAGS = $(ERROR_CFLAGS) $(COVERAGE_CFLAGS) + +rakia_includedir = $(includedir)/telepathy-rakia-0.7/rakia + +rakia_include_HEADERS = \ + base-connection.h \ + connection-aliasing.h \ + sofia-decls.h \ + codec-param-formats.h \ + event-target.h \ + handles.h \ + debug.h \ + media-manager.h \ + text-manager.h \ + util.h + +BUILT_SOURCES = \ + signals-marshal.h \ + signals-marshal.c + +librakia_la_SOURCES = \ + base-connection.c \ + base-connection-sofia.c \ + connection-aliasing.c \ + codec-param-formats.c \ + event-target.c \ + handles.c \ + debug.c \ + media-channel.h \ + media-channel.c \ + media-manager.c \ + media-session.h \ + media-session.c \ + media-stream.h \ + media-stream.c \ + text-channel.h \ + text-channel.c \ + text-manager.c \ + util.c + +nodist_librakia_la_SOURCES = \ + signals-marshal.h \ + signals-marshal.c + +EXTRA_DIST = signals-marshal.list + +CLEANFILES = $(BUILT_SOURCES) + +CLEANFILES += *.gcno + +signals-marshal.c: ${srcdir}/signals-marshal.list + glib-genmarshal --body --prefix=_rakia_marshal $< >$@ || rm -f $@ + +signals-marshal.h: ${srcdir}/signals-marshal.list + glib-genmarshal --header --prefix=_rakia_marshal $< >$@ || rm -f $@ diff --git a/rakia/base-connection-sofia.c b/rakia/base-connection-sofia.c new file mode 100644 index 0000000..de68ea5 --- /dev/null +++ b/rakia/base-connection-sofia.c @@ -0,0 +1,169 @@ +/* + * sip-connection-sofia.c - Source for RakiaConnection Sofia event handling + * Copyright (C) 2006-2007 Nokia Corporation + * Copyright (C) 2007-2008 Collabora Ltd. + * @author Kai Vehmanen <first.surname@nokia.com> + * @author Mikhail Zabaluev <mikhail.zabaluev@nokia.com> + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include <rakia/base-connection.h> +#include <sofia-sip/su_tag_io.h> + +#define DEBUG_FLAG TPSIP_DEBUG_EVENTS +#include "rakia/debug.h" + +static void +priv_r_shutdown(int status, + nua_t *nua) +{ + GSource *source; + gboolean source_recursive; + + if (status < 200) + return; + + /* Should be the source of the Sofia root */ + source = g_main_current_source (); + + /* XXX: temporarily allow recursion in the Sofia source to work around + * nua_destroy() requiring nested mainloop iterations to complete + * (Sofia-SIP bug #1624446). Actual recursion safety of the source is to be + * examined. */ + source_recursive = g_source_get_can_recurse (source); + if (!source_recursive) + { + DEBUG("forcing Sofia root GSource to be recursive"); + g_source_set_can_recurse (source, TRUE); + } + + DEBUG("destroying Sofia-SIP NUA at address %p", nua); + nua_destroy (nua); + + if (!source_recursive) + g_source_set_can_recurse (source, FALSE); +} + +#if 0 +static void +priv_r_unregister (int status, + char const *phrase, + nua_handle_t *nh) +{ + DEBUG("un-REGISTER got response: %03d %s", status, phrase); + + if (status < 200) + return; + + if (status == 401 || status == 407) + { + /* In SIP, de-registration can fail! However, there's not a lot we can + * do about this in the Telepathy model - once you've gone DISCONNECTED + * you're really not meant to go "oops, I'm still CONNECTED after all". + * So we ignore it and hope it goes away. */ + WARNING ("Registrar won't let me unregister: %d %s", status, phrase); + } +} +#endif + +#ifdef ENABLE_DEBUG +static void +priv_r_get_params (int status, + nua_t *nua, + nua_handle_t *nh, + tagi_t tags[]) +{ + if (status < 200) + return; + + if (nh != NULL) + return; + + /* note: print contents of all tags to stdout */ + tl_print(stdout, "Sofia-SIP NUA stack parameters:\n", tags); +} +#endif + +/** + * Callback for events delivered by the SIP stack. + * + * See libsofia-sip-ua/nua/nua.h documentation. + */ +void +rakia_base_connection_sofia_callback (nua_event_t event, + int status, + char const *phrase, + nua_t *nua, + RakiaBaseConnection *conn, + nua_handle_t *nh, + RakiaEventTarget *target, + sip_t const *sip, + tagi_t tags[]) +{ + DEBUG("event %s: %03d %s", + nua_event_name (event), status, phrase); + + switch (event) + { +#ifdef ENABLE_DEBUG + case nua_r_get_params: + priv_r_get_params (status, nua, nh, tags); + return; +#endif + case nua_r_shutdown: + priv_r_shutdown (status, nua); + return; + default: + break; + } + + g_assert (conn != NULL); + + DEBUG("connection %p, refcount %d", conn, ((GObject *)conn)->ref_count); + + { + RakiaNuaEvent ev = { + event, + status, + phrase, + nua, + nh, + sip + }; + + if (target == NULL) + { + target = (RakiaEventTarget *) conn; + DEBUG("dispatching to connection %p (unbound handle %p)", conn, nh); + } + else + { + g_assert (nh != NULL); + DEBUG("dispatching to target %p (handle %p)", target, nh); + } + + if (!rakia_event_target_emit_nua_event (target, + &ev, + tags)) + { + DEBUG("event %s for target %p was not consumed", nua_event_name (event), target); + } + } + + DEBUG ("exit"); +} diff --git a/rakia/base-connection.c b/rakia/base-connection.c new file mode 100644 index 0000000..597b5f5 --- /dev/null +++ b/rakia/base-connection.c @@ -0,0 +1,205 @@ +/* + * sip-base-connection.c - source for SipBaseConnection + * Copyright (C) 2011 Nokia Corporation. + * Copyright (C) 2005-2007 Collabora Ltd. + * Copyright (C) 2005-2011 Nokia Corporation + * @author Kai Vehmanen <first.surname@nokia.com> + * @author Martti Mela <first.surname@nokia.com> + * @author Mikhail Zabaluev <mikhail.zabaluev@nokia.com> + * @author Pekka Pessi <pekka.pessi@nokia.com> + * + * Based on rakia-connection and gabble implementation (gabble-connection). + * @author See gabble-connection.c + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include <rakia/base-connection.h> + +#include <telepathy-glib/telepathy-glib.h> +#include <rakia/sofia-decls.h> + +struct _RakiaBaseConnectionPrivate +{ + su_root_t *sofia_root; + + unsigned dispose_has_run:1; unsigned :0; +}; + +enum { + PROP_NONE, + PROP_SOFIA_ROOT, + PROP_SOFIA_NUA, +}; + +/* ---------------------------------------------------------------------- */ +/* GObject implementation */ + +static void event_target_iface_init (gpointer iface, gpointer data) {} + +G_DEFINE_TYPE_WITH_CODE (RakiaBaseConnection, + rakia_base_connection, TP_TYPE_BASE_CONNECTION, + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CONNECTION_INTERFACE_CONTACTS, + tp_contacts_mixin_iface_init); + G_IMPLEMENT_INTERFACE (TPSIP_TYPE_EVENT_TARGET, event_target_iface_init); +); + +static void +rakia_base_connection_init (RakiaBaseConnection *self) +{ + GObject *object = G_OBJECT (self); + TpBaseConnection *base = TP_BASE_CONNECTION (self); + + self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, TPSIP_TYPE_BASE_CONNECTION, + RakiaBaseConnectionPrivate); + + tp_contacts_mixin_init (object, + G_STRUCT_OFFSET (RakiaBaseConnection, contacts_mixin)); + + /* org.freedesktop.Telepathy.Connection attributes */ + tp_base_connection_register_with_contacts_mixin (base); +} + +static void +rakia_base_connection_constructed(GObject *object) +{ + if (G_OBJECT_CLASS(rakia_base_connection_parent_class)->constructed) + G_OBJECT_CLASS(rakia_base_connection_parent_class)->constructed(object); +} + +static void +rakia_base_connection_dispose(GObject *object) +{ + RakiaBaseConnection *self = TPSIP_BASE_CONNECTION(object); + + if (self->priv->dispose_has_run) + return; + self->priv->dispose_has_run = 1; + + G_OBJECT_CLASS(rakia_base_connection_parent_class)->dispose(object); +} + +void +rakia_base_connection_finalize(GObject *object) +{ + G_OBJECT_CLASS(rakia_base_connection_parent_class)->finalize(object); +} + +static void +rakia_base_connection_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + RakiaBaseConnection *self = TPSIP_BASE_CONNECTION (object); + RakiaBaseConnectionPrivate *priv = self->priv; + + switch (property_id) + { + case PROP_SOFIA_ROOT: + priv->sofia_root = g_value_get_pointer (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +rakia_base_connection_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + RakiaBaseConnection *self = TPSIP_BASE_CONNECTION (object); + RakiaBaseConnectionPrivate *priv = self->priv; + + switch (property_id) + { + case PROP_SOFIA_ROOT: + g_value_set_pointer (value, priv->sofia_root); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +/* -------------------------------------------------------------------------- */ + +static void +rakia_base_connection_class_init (RakiaBaseConnectionClass *klass) +{ + GObjectClass *object_class = (GObjectClass *) klass; + + g_type_class_add_private (klass, sizeof (RakiaBaseConnectionPrivate)); + + object_class->constructed = rakia_base_connection_constructed; + object_class->dispose = rakia_base_connection_dispose; + object_class->finalize = rakia_base_connection_finalize; + object_class->get_property = rakia_base_connection_get_property; + object_class->set_property = rakia_base_connection_set_property; + + g_object_class_install_property (object_class, + PROP_SOFIA_ROOT, + g_param_spec_pointer ("sofia-root", + "Sofia-SIP root", + "The root object for Sofia-SIP", + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (object_class, + PROP_SOFIA_NUA, + g_param_spec_pointer ("sofia-nua", + "Sofia-SIP UA", + "The UA object for Sofia-SIP", + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)); + + tp_contacts_mixin_class_init (object_class, + G_STRUCT_OFFSET(RakiaBaseConnectionClass, contacts_mixin_class)); +} + +nua_handle_t * +rakia_base_connection_create_handle (RakiaBaseConnection *self, + TpHandle tphandle) +{ + RakiaBaseConnectionClass *cls = TPSIP_BASE_CONNECTION_GET_CLASS (self); + + return cls->create_handle (self, tphandle); +} + +void +rakia_base_connection_add_auth_handler (RakiaBaseConnection *self, + RakiaEventTarget *target) +{ + RakiaBaseConnectionClass *cls = TPSIP_BASE_CONNECTION_GET_CLASS (self); + + if (cls->add_auth_handler) + cls->add_auth_handler (self, target); +} + +void +rakia_base_connection_save_event (RakiaBaseConnection *self, + nua_saved_event_t ret_saved [1]) +{ + nua_t *nua; + + g_object_get (self, "sofia-nua", &nua, NULL); + + nua_save_event (nua, ret_saved); +} diff --git a/rakia/base-connection.h b/rakia/base-connection.h new file mode 100644 index 0000000..dafb70f --- /dev/null +++ b/rakia/base-connection.h @@ -0,0 +1,92 @@ +/* + * sip-base-connection.h - Header for SipBaseConnection + * Copyright (C) 2005 Collabora Ltd. + * Copyright (C) 2005-2009 Nokia Corporation + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __TPSIP_BASE_CONNECTION_H__ +#define __TPSIP_BASE_CONNECTION_H__ + +#include <glib-object.h> + +#include <telepathy-glib/base-connection.h> +#include <telepathy-glib/contacts-mixin.h> + +#include <rakia/sofia-decls.h> +#include <rakia/event-target.h> + +G_BEGIN_DECLS + +typedef struct _RakiaBaseConnection RakiaBaseConnection; +typedef struct _RakiaBaseConnectionClass RakiaBaseConnectionClass; +typedef struct _RakiaBaseConnectionPrivate RakiaBaseConnectionPrivate; + +struct _RakiaBaseConnectionClass { + TpBaseConnectionClass parent_class; + TpContactsMixinClass contacts_mixin_class; + + nua_handle_t *(*create_handle) (RakiaBaseConnection *, TpHandle contact); + void (*add_auth_handler) (RakiaBaseConnection *, RakiaEventTarget *); +}; + +struct _RakiaBaseConnection { + TpBaseConnection parent; + TpContactsMixin contacts_mixin; + RakiaBaseConnectionPrivate *priv; +}; + +GType rakia_base_connection_get_type (void) G_GNUC_CONST; + +/* TYPE MACROS */ +#define TPSIP_TYPE_BASE_CONNECTION \ + (rakia_base_connection_get_type()) +#define TPSIP_BASE_CONNECTION(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), \ + TPSIP_TYPE_BASE_CONNECTION, RakiaBaseConnection)) +#define TPSIP_BASE_CONNECTION_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), \ + TPSIP_TYPE_BASE_CONNECTION, RakiaBaseConnectionClass)) +#define TPSIP_IS_BASE_CONNECTION(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), TPSIP_TYPE_BASE_CONNECTION)) +#define TPSIP_IS_BASE_CONNECTION_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), TPSIP_TYPE_BASE_CONNECTION)) +#define TPSIP_BASE_CONNECTION_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), \ + TPSIP_TYPE_BASE_CONNECTION, RakiaBaseConnectionClass)) + +/*********************************************************************** + * Functions for accessing Sofia-SIP interface handles + ***********************************************************************/ + +nua_handle_t *rakia_base_connection_create_handle (RakiaBaseConnection *, + TpHandle contact); +void rakia_base_connection_add_auth_handler (RakiaBaseConnection *self, + RakiaEventTarget *target); +void rakia_base_connection_save_event (RakiaBaseConnection *self, + nua_saved_event_t ret_saved [1]); + +/** Callback for events delivered by the SIP stack. */ +void rakia_base_connection_sofia_callback (nua_event_t event, + int status, char const *phrase, + nua_t *nua, RakiaBaseConnection *conn, + nua_handle_t *nh, RakiaEventTarget *target, + sip_t const *sip, + tagi_t tags[]); + +G_END_DECLS + +#endif /* #ifndef __TPSIP_BASE_CONNECTION_H__*/ diff --git a/rakia/codec-param-formats.c b/rakia/codec-param-formats.c new file mode 100644 index 0000000..5751a35 --- /dev/null +++ b/rakia/codec-param-formats.c @@ -0,0 +1,335 @@ +/* + * codec-param-formats.c - Implementation of codec parameter formatter infra + * Copyright (C) 2009, 2010 Nokia Corporation + * @author Mikhail Zabaluev <mikhail.zabaluev@nokia.com> + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "codec-param-formats.h" + +#include <string.h> + +#include <rakia/util.h> + +/* Regexps for the name and the value parts of the parameter syntax */ +#define FMTP_TOKEN_PARAM "[-A-Za-z0-9!#$%&'*+.^_`{|}~]+" +#define FMTP_TOKEN_VALUE "[^;\"\\s]+|\"([^\"\\\\]|\\\\.)*\"" +/* Indexes of the respective match groups in the whole regexp below */ +#define FMTP_MATCH_NAME_PARAM "p" +#define FMTP_MATCH_NAME_VALUE "v" + +typedef struct _RakiaCodecParamFormatting { + RakiaCodecParamFormatFunc format; + RakiaCodecParamParseFunc parse; +} RakiaCodecParamFormatting; + +static GRegex *fmtp_attr_regex = NULL; +static GRegex *dtmf_events_regex = NULL; + +static GHashTable *codec_param_formats[NUM_TP_MEDIA_STREAM_TYPES]; + +static void rakia_codec_param_formats_init (); + +/** + * rakia_codec_param_format: + * @media: the media type + * @name: name of the codec, as per its MIME subtype registration + * @params: the map of codec parameters + * @out: a #GString for the output + * + * Formats the parameters passed in the @params into a string suitable for + * <literal>a=fmtp</literal> attribute for an RTP payload description, + * as specified for the media type defined by @media and @name. + */ +void +rakia_codec_param_format (TpMediaStreamType media, const char *name, + GHashTable *params, GString *out) +{ + RakiaCodecParamFormatting *fmt; + + rakia_codec_param_formats_init (); + + /* XXX: thread unsafe, we don't care for now */ + fmt = g_hash_table_lookup (codec_param_formats[media], name); + + if (fmt != NULL && fmt->format != NULL) + fmt->format (params, out); + else + rakia_codec_param_format_generic (params, out); +} + +/** + * rakia_codec_param_parse: + * @media: the media type + * @name: name of the codec, as per its MIME subtype registration + * @fmtp: a string with the codec-specific parameter data. May be #NULL. + * @out: the parameter map to populate + * + * Parses the payload-specific parameter description as coming from an + * <literal>a=fmtp</literal> attribute of an RTP payload description. + * The media type is defined by @media and @name. + */ +void +rakia_codec_param_parse (TpMediaStreamType media, const char *name, + const gchar *fmtp, GHashTable *out) +{ + RakiaCodecParamFormatting *fmt; + + if (fmtp == NULL) + return; + + rakia_codec_param_formats_init (); + + /* XXX: thread unsafe, we don't care for now */ + fmt = g_hash_table_lookup (codec_param_formats[media], name); + + if (fmt != NULL && fmt->parse != NULL) + fmt->parse (fmtp, out); + else + rakia_codec_param_parse_generic (fmtp, out); +} + +/** + * rakia_codec_param_register_format: + * @media: the media type + * @name: name of the codec, as per its MIME subtype registration. Must be a static string. + * @format: pointer to the formatting function + * @parse: pointer to the parsing function + * + * Registers custom SDP payload parameter formatting routines for a media + * type. + */ +void +rakia_codec_param_register_format (TpMediaStreamType media, const char *name, + RakiaCodecParamFormatFunc format, + RakiaCodecParamParseFunc parse) +{ + RakiaCodecParamFormatting *fmt; + + rakia_codec_param_formats_init (); + + fmt = g_slice_new (RakiaCodecParamFormatting); + fmt->format = format; + fmt->parse = parse; + + /* XXX: thread unsafe, we don't care for now */ + g_hash_table_insert (codec_param_formats[media], (gpointer) name, fmt); +} + +static void +format_param_generic (gpointer key, gpointer val, gpointer user_data) +{ + const gchar *name = key; + const gchar *value = val; + GString *out = user_data; + + /* Ignore freaky parameters */ + g_return_if_fail (name != NULL && name[0]); + g_return_if_fail (value != NULL && value[0]); + + if (out->len != 0) + g_string_append_c (out, ';'); + + if (strpbrk (value, "; \t") == NULL) + g_string_append_printf (out, "%s=%s", name, value); + else + { + g_string_append (out, name); + g_string_append_c (out, '='); + rakia_string_append_quoted (out, value); + } +} + +/** + * rakia_codec_param_format_generic: + * @params: the map of codec parameters + * @out: a #GString for the output + * + * Formats the parameters as a semicolon separated list of + * <replaceable>parameter</replaceable><literal>=</literal><replaceable>value</replaceable> + * pairs, as recommended in IETF RFC 4855 Section 3. + */ +void +rakia_codec_param_format_generic (GHashTable *params, GString *out) +{ + g_hash_table_foreach (params, format_param_generic, out); +} + +/** + * rakia_codec_param_parse_generic: + * @fmtp: a string value with the parameter description + * @out: the parameter map to populate + * + * Parses parameters formatted as a semicolon separated list of + * <replaceable>parameter</replaceable><literal>=</literal><replaceable>value</replaceable> + * pairs, as recommended in IETF RFC 4855 Section 3. + */ +void +rakia_codec_param_parse_generic (const gchar *fmtp, GHashTable *out) +{ + GMatchInfo *match = NULL; + gint pos; + gint value_start; + gint value_end; + + if (fmtp == NULL) + return; + + pos = 0; + + /* Fast path for trivial cases, not involving the regex engine */ + while (g_ascii_isspace (fmtp[pos])) + ++pos; + if (!fmtp[pos]) + return; + + g_assert (fmtp_attr_regex != NULL); + + g_regex_match_full (fmtp_attr_regex, + fmtp, -1, pos, G_REGEX_MATCH_ANCHORED, &match, NULL); + + while (g_match_info_matches (match)) + { + gchar *name; + gchar *value; + + name = g_match_info_fetch_named (match, FMTP_MATCH_NAME_PARAM); + + g_match_info_fetch_named_pos (match, FMTP_MATCH_NAME_VALUE, + &value_start, &value_end); + + if (value_end - 1 > value_start + && fmtp[value_start] == '\"' && fmtp[value_end - 1] == '\"') + { + value = rakia_unquote_string (fmtp + value_start, + value_end - value_start); + } + else + { + value = g_strndup (fmtp + value_start, + value_end - value_start); + } + + g_hash_table_insert (out, name, value); + + g_match_info_fetch_pos (match, 0, NULL, &pos); + if (!fmtp[pos]) + break; + + g_match_info_next (match, NULL); + } + + g_match_info_free (match); + + if (fmtp[pos]) + g_message ("failed to parse part of format parameters" + " as an attribute-value list: %s", &fmtp[pos]); +} + +/* Custom format for audio/telephone-event */ + +static void +rakia_codec_param_format_telephone_event (GHashTable *params, GString *out) +{ + const gchar *events; + + /* events parameter value comes first without the parameter name */ + events = g_hash_table_lookup (params, "events"); + if (events != NULL) + { + g_string_append (out, events); + g_hash_table_remove (params, "events"); + } + + /* format the rest of the parameters, if any */ + rakia_codec_param_format_generic (params, out); +} + +static void +rakia_codec_param_parse_telephone_event (const gchar *fmtp, GHashTable *out) +{ + GMatchInfo *match = NULL; + gint end_pos = 0; + + g_assert (dtmf_events_regex != NULL); + + /* Parse the events list */ + + g_regex_match (dtmf_events_regex, fmtp, 0, &match); + + if (g_match_info_matches (match)) + { + gchar *events; + + events = g_match_info_fetch (match, 1); + g_hash_table_insert (out, g_strdup ("events"), events); + + g_match_info_fetch_pos (match, 0, NULL, &end_pos); + } + + g_match_info_free (match); + + /* Parse the remaining parameters, if any */ + rakia_codec_param_parse_generic (fmtp + end_pos, out); +} + +/* + * rakia_codec_param_formats_init: + * + * Initializes the codec parameter formatting infrastructure. + * This function must be called before using any other functions in this module. + * Calling the function more than once has no effect. + */ +static void +rakia_codec_param_formats_init () +{ + static volatile gsize been_here = 0; + + int i; + + if (g_once_init_enter (&been_here)) + g_once_init_leave (&been_here, 1); + else + return; + + for (i = 0; i < NUM_TP_MEDIA_STREAM_TYPES; ++i) + { + /* XXX: we ignore deallocation of values for now */ + codec_param_formats[i] = g_hash_table_new (g_str_hash, g_str_equal); + } + + rakia_codec_param_register_format ( + TP_MEDIA_STREAM_TYPE_AUDIO, "telephone-event", + rakia_codec_param_format_telephone_event, + rakia_codec_param_parse_telephone_event); + + fmtp_attr_regex = g_regex_new ( + "(?<" FMTP_MATCH_NAME_PARAM ">" FMTP_TOKEN_PARAM ")" + "\\s*=\\s*" + "(?<" FMTP_MATCH_NAME_VALUE ">" FMTP_TOKEN_VALUE ")" + "\\s*(;\\s*|$)", + G_REGEX_RAW | G_REGEX_OPTIMIZE, + 0, NULL); + g_assert (fmtp_attr_regex != NULL); + +#define DTMF_RANGE "[0-9]+(-[0-9]+)?" + + dtmf_events_regex = g_regex_new ( + "^(" DTMF_RANGE "(," DTMF_RANGE ")*)\\s*(;|$)", + G_REGEX_RAW | G_REGEX_OPTIMIZE, + 0, NULL); + g_assert (dtmf_events_regex != NULL); +} diff --git a/rakia/codec-param-formats.h b/rakia/codec-param-formats.h new file mode 100644 index 0000000..f5a717f --- /dev/null +++ b/rakia/codec-param-formats.h @@ -0,0 +1,80 @@ +/* + * codec-param-formats.h - Declarations for codec parameter formatters + * Copyright (C) 2009 Nokia Corporation + * @author Mikhail Zabaluev <mikhail.zabaluev@nokia.com> + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __TPSIP_CODEC_PARAM_FORMATS_H__ +#define __TPSIP_CODEC_PARAM_FORMATS_H__ + +#include <glib.h> + +#include <telepathy-glib/enums.h> + +G_BEGIN_DECLS + +/** + * RakiaCodecParamFormatFunc: + * @params: the map of codec parameters + * @out: a #GString for the output + * + * Defines the function pointer signature for codec parameter formatters. + * A formatter takes a codec parameter map as passed in + * a org.freedesktop.Telepathy.Media.StreamHandler codec structure, + * and outputs its SDP representation, as per the value for an + * <literal>a=fmtp</literal> attribute, into the string buffer @out. + * + * <note> + * <para>The function is allowed to delete pairs from the @params hash table. + * This is useful to implement a custom formatter that processes the + * few parameters treated specially, removes them from the map, and + * calls a more generic formatter such as rakia_codec_param_format_generic(). + * </para> + * </note> + */ +typedef void (* RakiaCodecParamFormatFunc) (GHashTable *params, GString *out); + +/** + * RakiaCodecParamParseFunc: + * @str: a string value with format-specific parameter description + * @out: the parameter map to populate + * + * Defines the function pointer signature for codec parameter parsers. + * A parser takes the string value coming from an <literal>a=fmtp</literal> + * SDP attribute, and populates the parameter hash table. + */ +typedef void (* RakiaCodecParamParseFunc) (const gchar *str, GHashTable *out); + +void rakia_codec_param_format (TpMediaStreamType media, const char *name, + GHashTable *params, GString *out); + +void rakia_codec_param_parse (TpMediaStreamType media, const char *name, + const gchar *fmtp, GHashTable *out); + +void rakia_codec_param_register_format ( + TpMediaStreamType media, + const char *name, + RakiaCodecParamFormatFunc format, + RakiaCodecParamParseFunc parse); + +void rakia_codec_param_format_generic (GHashTable *params, GString *out); + +void rakia_codec_param_parse_generic (const gchar *str, GHashTable *out); + +G_END_DECLS + +#endif /* !__TPSIP_CODEC_PARAM_FORMATS_H__ */ diff --git a/rakia/connection-aliasing.c b/rakia/connection-aliasing.c new file mode 100644 index 0000000..403f55e --- /dev/null +++ b/rakia/connection-aliasing.c @@ -0,0 +1,399 @@ +/* + * connection-aliasing.c - Implementation for RakiaConnectionAliasing interface + * Copyright (C) 2008-2011 Nokia Corporation + * @author Mikhail Zabaluev <mikhail.zabaluev@nokia.com> + * @author Pekka Pessi <pekka.pessi@nokia.com> + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include <rakia/connection-aliasing.h> +#include <rakia/base-connection.h> +#include <rakia/handles.h> + +#include <telepathy-glib/errors.h> +#include <telepathy-glib/gtypes.h> +#include <telepathy-glib/interfaces.h> +#include <telepathy-glib/svc-connection.h> +#include <telepathy-glib/contacts-mixin.h> + +#include "rakia/handles.h" + +#include <string.h> + +#define DEBUG_FLAG TPSIP_DEBUG_CONNECTION +#include "rakia/debug.h" + +enum { + PROP_NONE, + PROP_ALIAS, +}; + +static void +rakia_connection_aliasing_base_init (gpointer klass) +{ + static gboolean initialized = FALSE; + + if (!initialized) + { + initialized = TRUE; + + g_object_interface_install_property (klass, + g_param_spec_string ("alias", "Alias", + "User's display name", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + } +} + +GType +rakia_connection_aliasing_get_type (void) +{ + static GType type = 0; + + if (G_UNLIKELY (type == 0)) + { + static const GTypeInfo info = { + sizeof (RakiaConnectionAliasingInterface), + rakia_connection_aliasing_base_init, /* base_init */ + NULL, /* base_finalize */ + NULL, /* class_init */ + NULL, /* class_finalize */ + NULL, /* class_data */ + 0, + 0, /* n_preallocs */ + NULL /* instance_init */ + }; + + type = g_type_register_static (G_TYPE_INTERFACE, + "RakiaConnectionAliasingInterface", &info, 0); + + g_type_interface_add_prerequisite (type, TPSIP_TYPE_BASE_CONNECTION); + g_type_interface_add_prerequisite (type, + TP_TYPE_SVC_CONNECTION_INTERFACE_ALIASING); + } + + return type; +} + +static void +rakia_connection_get_alias_flags (TpSvcConnectionInterfaceAliasing *iface, + DBusGMethodInvocation *context) +{ + TpBaseConnection *base = TP_BASE_CONNECTION (iface); + + TP_BASE_CONNECTION_ERROR_IF_NOT_CONNECTED (base, context); + + /* No server-side aliasing yet */ + tp_svc_connection_interface_aliasing_return_from_get_alias_flags ( + context, 0); +} + +static gchar * +conn_get_default_alias (TpBaseConnection *base, + TpHandleRepoIface *contact_handles, + TpHandle handle) +{ + const url_t *url; + gchar *alias = NULL; + + /* TODO: create our custom handle repo to be able to get the URL off it. + * Then we can reuse the contact_handles parameter */ + url = rakia_handle_inspect_uri (base, handle); + + switch (url->url_type) + { + case url_sip: + /* Return the SIP URI stripped down to [user@]host */ + if (url->url_user != NULL) + alias = g_strdup_printf ("%s@%s", + url->url_user, url->url_host); + else + alias = g_strdup (url->url_host); + break; + case url_tel: + /* Retrieve the telephone number */ + alias = g_strdup (url->url_host); + break; + default: + /* Return the handle string as is */ + alias = g_strdup (tp_handle_inspect (contact_handles, handle)); + } + return alias; +} + +static gchar * +conn_get_alias (TpBaseConnection *base, + TpHandleRepoIface *contact_handles, + TpHandle handle) +{ + gchar *alias = NULL; + + if (handle == base->self_handle) + { + /* Get our user-settable alias from the connection property */ + g_object_get (base, "alias", &alias, NULL); + } + + if (alias == NULL) + alias = conn_get_default_alias (base, contact_handles, handle); + + g_assert (alias != NULL); + DEBUG("handle %u got alias %s", handle, alias); + + return alias; +} + +static void +rakia_connection_request_aliases (TpSvcConnectionInterfaceAliasing *iface, + const GArray *contacts, + DBusGMethodInvocation *context) +{ + TpBaseConnection *base = TP_BASE_CONNECTION (iface); + TpHandleRepoIface *contact_handles; + GArray *aliases; + gchar **res; + GError *error = NULL; + guint i; + + TP_BASE_CONNECTION_ERROR_IF_NOT_CONNECTED (base, context); + + contact_handles = tp_base_connection_get_handles (base, + TP_HANDLE_TYPE_CONTACT); + + if (!tp_handles_are_valid (contact_handles, contacts, FALSE, &error)) + { + dbus_g_method_return_error (context, error); + g_error_free (error); + return; + } + + aliases = g_array_sized_new (TRUE, FALSE, sizeof (gchar *), contacts->len); + + for (i = 0; i < contacts->len; i++) + { + TpHandle handle; + gchar *alias; + + handle = g_array_index (contacts, TpHandle, i); + + alias = conn_get_alias (base, contact_handles, handle); + + g_array_append_val (aliases, alias); + } + + res = (gchar **) g_array_free (aliases, FALSE); + + tp_svc_connection_interface_aliasing_return_from_request_aliases ( + context, (const gchar **) res); + + g_strfreev (res); +} + +static void +rakia_connection_get_aliases (TpSvcConnectionInterfaceAliasing *iface, + const GArray *contacts, + DBusGMethodInvocation *context) +{ + TpBaseConnection *base = TP_BASE_CONNECTION (iface); + TpHandleRepoIface *contact_handles; + GHashTable *result; + GError *error = NULL; + guint i; + + TP_BASE_CONNECTION_ERROR_IF_NOT_CONNECTED (base, context); + + contact_handles = tp_base_connection_get_handles (base, + TP_HANDLE_TYPE_CONTACT); + + if (!tp_handles_are_valid (contact_handles, contacts, FALSE, &error)) + { + dbus_g_method_return_error (context, error); + g_error_free (error); + return; + } + + result = g_hash_table_new_full (g_direct_hash, g_direct_equal, + NULL, g_free); + + for (i = 0; i < contacts->len; i++) + { + TpHandle handle; + gchar *alias; + + handle = g_array_index (contacts, TpHandle, i); + + alias = conn_get_alias (base, contact_handles, handle); + + g_hash_table_insert (result, GUINT_TO_POINTER (handle), alias); + } + + tp_svc_connection_interface_aliasing_return_from_get_aliases (context, + result); + + g_hash_table_destroy (result); +} + +static void +emit_self_alias_change (TpBaseConnection *base, const gchar *alias) +{ + GPtrArray *change_data; + GValue change_pair = { 0, }; + + g_value_init (&change_pair, TP_STRUCT_TYPE_ALIAS_PAIR); + g_value_take_boxed (&change_pair, + dbus_g_type_specialized_construct (TP_STRUCT_TYPE_ALIAS_PAIR)); + dbus_g_type_struct_set (&change_pair, + 0, base->self_handle, + 1, alias, + G_MAXUINT); + change_data = g_ptr_array_sized_new (1); + g_ptr_array_add (change_data, g_value_get_boxed (&change_pair)); + + tp_svc_connection_interface_aliasing_emit_aliases_changed (base, change_data); + + g_ptr_array_free (change_data, TRUE); + g_value_unset (&change_pair); +} + +static const gchar * +collapse_whitespace (const gchar *str, gchar **to_free) +{ + static GRegex *whitespace_regex = NULL; + + const gchar *p; + gchar *subst_res; + + p = (const gchar *) strpbrk (str, " \t\r\n"); + if (p == NULL) + return str; + + if (whitespace_regex == NULL) + { + whitespace_regex = g_regex_new ("[[:space:]]+", + G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, NULL); + } + + subst_res = g_regex_replace_literal (whitespace_regex, str, -1, p - str, " ", + 0, NULL); + + *to_free = subst_res; + return subst_res; +} + +static void +rakia_connection_set_aliases (TpSvcConnectionInterfaceAliasing *iface, + GHashTable *aliases, + DBusGMethodInvocation *context) +{ + TpBaseConnection *base = TP_BASE_CONNECTION (iface); + TpHandleRepoIface *contact_handles; + const gchar *alias; + gchar *default_alias; + gchar *to_free = NULL; + + TP_BASE_CONNECTION_ERROR_IF_NOT_CONNECTED (base, context); + + /* We only care about the self alias */ + alias = g_hash_table_lookup (aliases, GINT_TO_POINTER (base->self_handle)); + + if (alias == NULL || g_hash_table_size (aliases) > 1) + { + /* One of the handles (if there are any) cannot be the self handle */ + GError err = { TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "Cannot set aliases for any contact except self" }; + dbus_g_method_return_error (context, &err); + return; + } + + alias = collapse_whitespace (alias, &to_free); + + contact_handles = tp_base_connection_get_handles (base, + TP_HANDLE_TYPE_CONTACT); + default_alias = conn_get_default_alias (base, + contact_handles, base->self_handle); + + if (strcmp (alias, default_alias) == 0) + { + DEBUG("using default alias for self"); + g_object_set (base, "alias", NULL, NULL); + } + else + { + DEBUG("setting alias for self: %s", alias); + g_object_set (base, "alias", alias, NULL); + } + + emit_self_alias_change (base, alias); + + g_free (default_alias); + g_free (to_free); + + tp_svc_connection_interface_aliasing_return_from_set_aliases (context); +} + +static void +rakia_conn_aliasing_fill_contact_attributes (GObject *obj, + const GArray *contacts, GHashTable *attributes_hash) +{ + TpBaseConnection *base = TP_BASE_CONNECTION (obj); + TpHandleRepoIface *contact_handles; + guint i; + + contact_handles = tp_base_connection_get_handles (base, + TP_HANDLE_TYPE_CONTACT); + + for (i = 0; i < contacts->len; i++) + { + TpHandle handle; + GValue *val; + + handle = g_array_index (contacts, TpHandle, i); + + val = tp_g_value_slice_new (G_TYPE_STRING); + + g_value_take_string (val, + conn_get_alias (base, contact_handles, handle)); + + tp_contacts_mixin_set_contact_attribute (attributes_hash, handle, + TP_IFACE_CONNECTION_INTERFACE_ALIASING "/alias", val); + } +} + +void +rakia_connection_aliasing_init (gpointer instance) +{ + tp_contacts_mixin_add_contact_attributes_iface (G_OBJECT (instance), + TP_IFACE_CONNECTION_INTERFACE_ALIASING, + rakia_conn_aliasing_fill_contact_attributes); +} + + +void +rakia_connection_aliasing_svc_iface_init (gpointer g_iface, gpointer iface_data) +{ + TpSvcConnectionInterfaceAliasingClass *klass = + (TpSvcConnectionInterfaceAliasingClass *) g_iface; + +#define IMPLEMENT(x) tp_svc_connection_interface_aliasing_implement_##x (\ + klass, rakia_connection_##x) + IMPLEMENT(get_alias_flags); + IMPLEMENT(request_aliases); + IMPLEMENT(get_aliases); + IMPLEMENT(set_aliases); +#undef IMPLEMENT +} diff --git a/rakia/connection-aliasing.h b/rakia/connection-aliasing.h new file mode 100644 index 0000000..bc30565 --- /dev/null +++ b/rakia/connection-aliasing.h @@ -0,0 +1,58 @@ +/* + * rakia/connection-aliasing.h - Aliasing interface implementation for SIP + * Copyright (C) 2008, 2011 Nokia Corporation + * @author Mikhail Zabaluev <mikhail.zabaluev@nokia.com> + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __TPSIP_CONNECTION_ALIASING_H__ +#define __TPSIP_CONNECTION_ALIASING_H__ + +#include <glib-object.h> + +G_BEGIN_DECLS + +typedef struct _RakiaConnectionAliasing RakiaConnectionAliasing; + +typedef struct _RakiaConnectionAliasingInterface +RakiaConnectionAliasingInterface; + +/* TYPE MACROS */ +#define TPSIP_TYPE_CONNECTION_ALIASING \ + (rakia_connection_aliasing_get_type ()) +#define TPSIP_CONNECTION_ALIASING(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), \ + TPSIP_TYPE_CONNECTION_ALIASING, RakiaConnectionAliasing)) +#define TPSIP_IS_CONNECTION_ALIASING(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), TPSIP_TYPE_CONNECTION_ALIASING)) +#define TPSIP_CONNECTION_ALIASING_GET_INTERFACE(obj) \ + (G_TYPE_INSTANCE_GET_INTERFACE((obj), \ + TPSIP_TYPE_CONNECTION_ALIASING, RakiaConnectionAliasingInterface)) + +struct _RakiaConnectionAliasingInterface { + GTypeInterface base_iface; +}; + +GType rakia_connection_aliasing_get_type (void) G_GNUC_CONST; + +void rakia_connection_aliasing_init (gpointer instance); + +void rakia_connection_aliasing_svc_iface_init (gpointer g_iface, + gpointer iface_data); + +G_END_DECLS + +#endif /*__TPSIP_CONN_ALIASING_H__*/ diff --git a/rakia/debug.c b/rakia/debug.c new file mode 100644 index 0000000..11f7b6a --- /dev/null +++ b/rakia/debug.c @@ -0,0 +1,197 @@ +/* + * debug.h - Debug helpers for Telepathy-SofiaSIP, implementation + * Copyright (C) 2007-2008 Nokia Corporation + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include <stdarg.h> + +#include <glib.h> + +#include <telepathy-glib/debug.h> +#include <telepathy-glib/debug-sender.h> + +#include "rakia/debug.h" + +#include <sofia-sip/su_log.h> + +static RakiaDebugFlags rakia_debug_flags = 0; + +static const GDebugKey rakia_debug_keys[] = { + { "media-channel", TPSIP_DEBUG_MEDIA }, + { "connection", TPSIP_DEBUG_CONNECTION }, + { "im", TPSIP_DEBUG_IM }, + { "events", TPSIP_DEBUG_EVENTS }, + { "sofia", TPSIP_DEBUG_SOFIA }, +}; + +void rakia_debug_set_flags_from_env () +{ + const gchar *flags_string; + + flags_string = g_getenv ("TPSIP_DEBUG"); + if (flags_string == NULL) + flags_string = g_getenv ("RAKIA_DEBUG"); + + if (flags_string != NULL) + { + tp_debug_set_flags (flags_string); + + rakia_debug_set_flags (g_parse_debug_string (flags_string, + rakia_debug_keys, + G_N_ELEMENTS(rakia_debug_keys))); + } +} + +void rakia_debug_set_flags (RakiaDebugFlags new_flags) +{ + rakia_debug_flags |= new_flags; +} + +gboolean rakia_debug_flag_is_set (RakiaDebugFlags flag) +{ + return (flag & rakia_debug_flags) ? TRUE : FALSE; +} + +static GHashTable *flag_to_domains = NULL; + +static const gchar * +debug_flag_to_domain (RakiaDebugFlags flag) +{ + if (G_UNLIKELY (flag_to_domains == NULL)) + { + guint i; + + flag_to_domains = g_hash_table_new_full (g_direct_hash, g_direct_equal, + NULL, g_free); + + for (i = 0; i < G_N_ELEMENTS(rakia_debug_keys); i++) + { + GDebugKey key = (GDebugKey) rakia_debug_keys[i]; + gchar *val; + + val = g_strdup_printf ("%s/%s", "rakia", key.key); + g_hash_table_insert (flag_to_domains, + GUINT_TO_POINTER (key.value), val); + } + } + + return g_hash_table_lookup (flag_to_domains, GUINT_TO_POINTER (flag)); +} + +void +rakia_debug_free (void) +{ + if (flag_to_domains == NULL) + return; + + g_hash_table_destroy (flag_to_domains); + flag_to_domains = NULL; +} + +static void +log_to_debug_sender (RakiaDebugFlags flag, + GLogLevelFlags level, + const gchar *message) +{ + TpDebugSender *dbg; + GTimeVal now; + + dbg = tp_debug_sender_dup (); + + g_get_current_time (&now); + + tp_debug_sender_add_message (dbg, &now, debug_flag_to_domain (flag), + level, message); + + g_object_unref (dbg); +} + +void rakia_log (RakiaDebugFlags flag, + GLogLevelFlags level, + const gchar *format, + ...) +{ + gchar *message; + va_list args; + + va_start (args, format); + message = g_strdup_vprintf (format, args); + va_end (args); + + log_to_debug_sender (flag, level, message); + + if (flag & rakia_debug_flags) + g_log (G_LOG_DOMAIN, level, "%s", message); + + g_free (message); +} + +static void +rakia_sofia_log_handler (void *logdata, const char *format, va_list args) +{ +#ifdef ENABLE_DEBUG + GString *buf = (GString *)logdata; + g_assert (buf != NULL); + + /* Append the formatted message at the end of the buffer */ + g_string_append_vprintf (buf, format, args); + + /* If we have a terminated line, log it, stripping the newline */ + if (buf->str[buf->len - 1] == '\n') + { + g_string_truncate (buf, buf->len - 1); + rakia_log (TPSIP_DEBUG_SOFIA, G_LOG_LEVEL_DEBUG, "%s", buf->str); + g_string_truncate (buf, 0); + } +#endif +} + +gpointer +rakia_sofia_log_init () +{ + GString *buf; + +#ifdef ENABLE_DEBUG + buf = g_string_sized_new (80); +#else + buf = NULL; +#endif + + su_log_redirect (NULL, rakia_sofia_log_handler, buf); + + return buf; +} + +void +rakia_sofia_log_finalize (gpointer logdata) +{ +#ifdef ENABLE_DEBUG + GString *buf = (GString *)logdata; + + if (buf->len != 0) + { + /* Don't use rakia_log here because the CM has already been finalized, so + * out TpDebugSender will have too. It isn't crucial, anyway. */ + g_debug ("%s", buf->str); + g_message ("last Sofia log message was not newline-terminated"); + } + + g_string_free (buf, TRUE); +#endif +} diff --git a/rakia/debug.h b/rakia/debug.h new file mode 100644 index 0000000..58d7628 --- /dev/null +++ b/rakia/debug.h @@ -0,0 +1,70 @@ +/* + * debug.h - Debug helpers for Telepathy-SofiaSIP, headers + * Copyright (C) 2007-2008 Nokia Corporation + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __DEBUG_H__ +#define __DEBUG_H__ + +#include <glib.h> + +G_BEGIN_DECLS + +typedef enum +{ + TPSIP_DEBUG_CONNECTION = 1 << 0, + TPSIP_DEBUG_MEDIA = 1 << 1, + TPSIP_DEBUG_IM = 1 << 2, + TPSIP_DEBUG_EVENTS = 1 << 3, + TPSIP_DEBUG_SOFIA = 1 << 4, +} RakiaDebugFlags; + +void rakia_debug_set_flags_from_env (); +void rakia_debug_set_flags (RakiaDebugFlags flags); +gboolean rakia_debug_flag_is_set (RakiaDebugFlags flag); +void rakia_log (RakiaDebugFlags flag, GLogLevelFlags level, + const gchar *format, ...) G_GNUC_PRINTF (3, 4); +void rakia_debug_free (void); + +gpointer rakia_sofia_log_init (); +void rakia_sofia_log_finalize (gpointer logdata); + +G_END_DECLS + +#ifdef DEBUG_FLAG + +#define DEBUG(format, ...) \ + rakia_log(DEBUG_FLAG, G_LOG_LEVEL_DEBUG, "%s: " format, \ + G_STRFUNC, ##__VA_ARGS__) +#define WARNING(format, ...) \ + rakia_log(DEBUG_FLAG, G_LOG_LEVEL_WARNING, "%s: " format, \ + G_STRFUNC, ##__VA_ARGS__) +#define MESSAGE(format, ...) \ + rakia_log(DEBUG_FLAG, G_LOG_LEVEL_MESSAGE, "%s: " format, \ + G_STRFUNC, ##__VA_ARGS__) + +/* #define DEBUGGING rakia_debug_flag_is_set(DEBUG_FLAG) */ + +#else /* DEBUG_FLAG */ + +#define DEBUG(format, ...) +#define WARNING(format, ...) +#define MESSAGE(format, ...) + +#endif /* DEBUG_FLAG */ + +#endif /* __DEBUG_H__ */ diff --git a/rakia/event-target.c b/rakia/event-target.c new file mode 100644 index 0000000..74bec43 --- /dev/null +++ b/rakia/event-target.c @@ -0,0 +1,271 @@ +/* + * event-target.c - Implementation for RakiaEventTarget interface + * Copyright (C) 2008 Nokia Corporation + * @author Mikhail Zabaluev <mikhail.zabaluev@nokia.com> + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "event-target.h" +#include "signals-marshal.h" + +/* Define to the highest known nua_event_e enumeration member */ +#define TPSIP_NUA_EVENT_LAST nua_i_register + +/* Mapping of the event enumeration to signal detail quarks */ +static GQuark event_quarks[TPSIP_NUA_EVENT_LAST + 1] = {0}; + +/* Signals */ +enum { + SIG_NUA_EVENT, + NUM_SIGNALS +}; +static guint signals[NUM_SIGNALS] = {0}; + +static RakiaEventTarget * rakia_event_target_gone_instance (); + +static void +rakia_event_target_base_init (gpointer klass) +{ + static gboolean initialized = FALSE; + gint i; + + if (!initialized) + { + initialized = TRUE; + + /** + * RakiaEventTarget::nua-event: + * @instance: an object implementing #RakiaEventTarget that emitted the signal + * @event: Pointer to the event data structure + * @tags: Tag list containing dynamically typed information about the event + * + * Emitted by the NUA event handler for an object bound + * to a NUA operation handle. + * Returns: a handler returns TRUE to indicate that further handling + * of the signal should cease. + */ + signals[SIG_NUA_EVENT] = + g_signal_new ("nua-event", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST|G_SIGNAL_DETAILED, + 0 /* G_STRUCT_OFFSET (RakiaEventTargetInterface, nua_event) */, + g_signal_accumulator_true_handled, + NULL, + _rakia_marshal_BOOLEAN__POINTER_POINTER, + G_TYPE_BOOLEAN, + 2, + G_TYPE_POINTER, + G_TYPE_POINTER); + + for (i = 0; i <= TPSIP_NUA_EVENT_LAST; i++) + event_quarks[i] = + g_quark_from_static_string (nua_event_name ((nua_event_t) i)); + } +} + +GType +rakia_event_target_get_type (void) +{ + static GType type = 0; + + if (G_UNLIKELY (type == 0)) + { + static const GTypeInfo info = { + sizeof (RakiaEventTargetInterface), + rakia_event_target_base_init, /* base_init */ + NULL, /* base_finalize */ + NULL, /* class_init */ + NULL, /* class_finalize */ + NULL, /* class_data */ + 0, + 0, /* n_preallocs */ + NULL /* instance_init */ + }; + + type = g_type_register_static (G_TYPE_INTERFACE, + "RakiaEventTargetInterface", &info, 0); + } + + return type; +} + +static void +rakia_event_target_retire_nua_handle (nua_handle_t *nh) +{ + static RakiaEventTarget *target_gone = NULL; + + if (G_UNLIKELY (target_gone == NULL)) + target_gone = rakia_event_target_gone_instance (); + + nua_handle_bind (nh, target_gone); + nua_handle_unref (nh); +} + +static void +_rakia_event_target_finalized (gpointer data, GObject *former_obj) +{ + rakia_event_target_retire_nua_handle ((nua_handle_t *) data); +} + +/** + * rakia_event_target_attach: + * @nh: The NUA handle + * @obj: an object implementing #RakiaEventTarget + * + * Attach an event target object to the NUA handle using nua_handle_bind(). + * The reference count of the NUA handle is incremented. + * When the attached object is finalized, the reference count of the NUA handle + * is decremented, and the handle is bound to a special end-of-life event + * handler for debugging purposes. + */ +void +rakia_event_target_attach (nua_handle_t *nh, GObject *obj) +{ + g_assert (nh != NULL); + + nua_handle_bind (nh, TPSIP_EVENT_TARGET (obj)); + nua_handle_ref (nh); + + g_object_weak_ref (obj, _rakia_event_target_finalized, nh); +} + +/** + * rakia_event_target_detach: + * @nh: The NUA handle + * + * Detach the event target object previously attached to the NUA handle + * using rakia_event_target_attach(). + * The reference count of the NUA handle is decremented, and the handle + * is bound to a special end-of-life event handler for debugging purposes. + */ +void +rakia_event_target_detach (nua_handle_t *nh) +{ + GObject *obj; + + g_assert (nh != NULL); + + obj = G_OBJECT (nua_handle_magic (nh)); + g_object_weak_unref (obj, _rakia_event_target_finalized, nh); + + rakia_event_target_retire_nua_handle (nh); +} + +/** + * rakia_event_target_emit_nua_event: + * @instance: The object implementing this interface + * @event: Pointer to the event data structure + * @tags: Tag list containing dynamically typed information about the event + * + * Emit the signal #RakiaEventTarget::nua-event, detailed with the event name, + * on an instance of a class implementing this interface. + * This function is normally called by the NUA callback. + * Returns: TRUE if a signal handler handled the event and returned TRUE. + */ +gboolean +rakia_event_target_emit_nua_event (gpointer instance, + const RakiaNuaEvent *ev, + tagi_t tags[]) +{ + gboolean retval = FALSE; + gint nua_event; + GQuark detail; + + g_assert (TPSIP_IS_EVENT_TARGET (instance)); + + nua_event = ev->nua_event; + + detail = G_LIKELY (nua_event >= 0 && nua_event <= TPSIP_NUA_EVENT_LAST) + ? event_quarks[nua_event] + : g_quark_from_static_string (nua_event_name (nua_event)); + + g_signal_emit (instance, + signals[SIG_NUA_EVENT], + detail, + ev, + tags, + &retval); + + return retval; +} + +/* RakiaEventTargetGone: + * a special private implementation of RakiaEventTarget for a singleton + * catch-all object to associate with handles that have been detached from + * other event targets. + */ +typedef struct _RakiaEventTargetGone RakiaEventTargetGone; +typedef struct _RakiaEventTargetGoneClass RakiaEventTargetGoneClass; + +struct _RakiaEventTargetGone { + GObject parent; +}; + +struct _RakiaEventTargetGoneClass { + GObjectClass parent_class; +}; + +static gboolean +rakia_late_nua_event_cb (RakiaEventTargetGone *self, + const RakiaNuaEvent *event, + tagi_t tags[], + gpointer foo) +{ + g_message ("%s received for the retired handle %p: %03d %s", + nua_event_name (event->nua_event), + event->nua_handle, + event->status, + event->text); + return TRUE; +} + +static void +rakia_event_target_gone_iface_init (gpointer g_iface, gpointer iface_data) +{ +} + +G_DEFINE_TYPE_WITH_CODE (RakiaEventTargetGone, rakia_event_target_gone, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (TPSIP_TYPE_EVENT_TARGET, rakia_event_target_gone_iface_init)) + +static void +rakia_event_target_gone_class_init (RakiaEventTargetGoneClass *klass) +{ +} + +static void +rakia_event_target_gone_init (RakiaEventTargetGone *self) +{ + g_signal_connect (self, "nua-event", + G_CALLBACK (rakia_late_nua_event_cb), + NULL); +} + +static gpointer +_rakia_event_target_gone_new_instance (gpointer foo) +{ + return g_object_new (rakia_event_target_gone_get_type (), NULL); +} + +static RakiaEventTarget * +rakia_event_target_gone_instance () +{ + static GOnce init_gone_once = G_ONCE_INIT; + + g_once (&init_gone_once, _rakia_event_target_gone_new_instance, NULL); + + return TPSIP_EVENT_TARGET (init_gone_once.retval); +} diff --git a/rakia/event-target.h b/rakia/event-target.h new file mode 100644 index 0000000..c6aae9c --- /dev/null +++ b/rakia/event-target.h @@ -0,0 +1,92 @@ +/* + * event-target.h - Header for RakiaEventTarget interface and related utilities + * Copyright (C) 2008 Nokia Corporation + * @author Mikhail Zabaluev <mikhail.zabaluev@nokia.com> + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __TPSIP_EVENT_TARGET_H__ +#define __TPSIP_EVENT_TARGET_H__ + +#include <glib-object.h> +#include <rakia/sofia-decls.h> + +G_BEGIN_DECLS + +typedef struct _RakiaNuaEvent RakiaNuaEvent; + +/** + * RakiaNuaEvent: + * @nua_event: The NUA event identifier + * @status: a SIP status code, or a status value used by Sofia-SIP + * @text: The text corresponding to the status code + * @nua: Pointer to the NUA stack + * @nua_handle: The NUA operation handle for the event + * @sip: Headers in the parsed incoming message, or NULL + * + * This structure contains data passed to the NUA event callback. + * The event tag list is not included and passed as a separate parameter. + */ +struct _RakiaNuaEvent { + nua_event_t nua_event; + gint status; + const gchar *text; + nua_t *nua; + nua_handle_t *nua_handle; + const sip_t *sip; +}; + +/** + * RakiaEventTarget: + * + * A typedef representing any implementation of this interface. + */ +typedef struct _RakiaEventTarget RakiaEventTarget; + +typedef struct _RakiaEventTargetInterface RakiaEventTargetInterface; + +/* TYPE MACROS */ +#define TPSIP_TYPE_EVENT_TARGET \ + (rakia_event_target_get_type ()) +#define TPSIP_EVENT_TARGET(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), TPSIP_TYPE_EVENT_TARGET, RakiaEventTarget)) +#define TPSIP_IS_EVENT_TARGET(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), TPSIP_TYPE_EVENT_TARGET)) +#define TPSIP_EVENT_TARGET_GET_INTERFACE(obj) \ + (G_TYPE_INSTANCE_GET_INTERFACE((obj), TPSIP_TYPE_EVENT_TARGET, RakiaEventTargetInterface)) + +struct _RakiaEventTargetInterface { + GTypeInterface base_iface; + +/* + gboolean (* nua_event) (RakiaEventTarget *self, + const RakiaEvent *event, + tagi_t tags[]); +*/ +}; + +GType rakia_event_target_get_type (void) G_GNUC_CONST; + +void rakia_event_target_attach (nua_handle_t *nh, GObject *target); +void rakia_event_target_detach (nua_handle_t *nh); + +gboolean rakia_event_target_emit_nua_event (gpointer instance, + const RakiaNuaEvent *event, + tagi_t tags[]); + +G_END_DECLS + +#endif /*__TPSIP_EVENT_TARGET_H__*/ diff --git a/rakia/handles.c b/rakia/handles.c new file mode 100644 index 0000000..5031dd0 --- /dev/null +++ b/rakia/handles.c @@ -0,0 +1,345 @@ +/* + * rakia/handles.c - Handler helpers + * Copyright (C) 2005 Collabora Ltd. + * Copyright (C) 2006-2011 Nokia Corporation + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include <stdlib.h> + +#include <rakia/handles.h> +#include <sofia-sip/sip_header.h> + +#define DEBUG_FLAG TPSIP_DEBUG_CONNECTION +#include "rakia/debug.h" + +static GQuark +rakia_handle_url_quark () +{ + static GQuark quark = 0; + + if (G_UNLIKELY (quark == 0)) + quark = g_quark_from_static_string ("rakia-handle-url"); + + return quark; +} + +const url_t* +rakia_handle_inspect_uri (TpBaseConnection *base, + TpHandle handle) +{ + TpHandleRepoIface *repo; + GQuark url_quark; + url_t *url; + GError *error; + + repo = tp_base_connection_get_handles (base, TP_HANDLE_TYPE_CONTACT); + + if (!tp_handle_is_valid (repo, handle, &error)) + { + DEBUG("invalid handle %u: %s", handle, error->message); + g_error_free (error); + return NULL; + } + + url_quark = rakia_handle_url_quark (); + + url = tp_handle_get_qdata (repo, handle, url_quark); + + if (url == NULL) + { + url = url_make (NULL, tp_handle_inspect (repo, handle)); + + tp_handle_set_qdata (repo, handle, url_quark, url, free); + } + + return url; +} + +TpHandle +rakia_handle_ensure (TpBaseConnection *conn, + url_t const *uri, + char const *alias) +{ + TpHandleRepoIface *repo; + gchar *str; + TpHandle handle; + + g_return_val_if_fail (TP_IS_BASE_CONNECTION (conn), 0); + g_return_val_if_fail (uri != NULL, 0); + + repo = tp_base_connection_get_handles (conn, TP_HANDLE_TYPE_CONTACT); + + str = url_as_string (NULL, uri); + + handle = tp_handle_ensure (repo, str, NULL, NULL); + + su_free (NULL, str); + + /* TODO: set qdata for the alias */ + + return handle; +} + +TpHandle +rakia_handle_by_requestor (TpBaseConnection *conn, + sip_t const *sip) +{ + url_t const *uri; + char const *display; + + g_return_val_if_fail (sip != NULL, 0); + g_return_val_if_fail (sip->sip_from != NULL, 0); + + uri = sip->sip_from->a_url; + display = sip->sip_from->a_display; + + return rakia_handle_ensure (conn, uri, display); +} + +void +rakia_handle_unref (TpBaseConnection *conn, + TpHandle handle) +{ + TpHandleRepoIface *repo; + + g_return_if_fail (TP_IS_BASE_CONNECTION (conn)); + g_return_if_fail (handle != 0); + + repo = tp_base_connection_get_handles (conn, TP_HANDLE_TYPE_CONTACT); + + tp_handle_unref (repo, handle); +} + +char const * +rakia_handle_inspect (TpBaseConnection *conn, + TpHandle handle) +{ + TpHandleRepoIface *repo; + + g_return_val_if_fail (TP_IS_BASE_CONNECTION (conn), NULL); + g_return_val_if_fail (handle != 0, NULL); + + repo = tp_base_connection_get_handles (conn, TP_HANDLE_TYPE_CONTACT); + + return tp_handle_inspect (repo, handle); +} + +static gboolean +priv_is_host (const gchar* str) +{ + static GRegex *host_regex = NULL; + +#define DOMAIN "[a-z0-9]([-a-z0-9]*[a-z0-9])?" +#define TLD "[a-z]([-a-z0-9]*[a-z0-9])?" + + if (host_regex == NULL) + { + GError *error = NULL; + + host_regex = g_regex_new ("^(" + "("DOMAIN"\\.)*"TLD"\\.?|" /* host name */ + "[0-9]{1,3}(\\.[0-9]{1,3}){3}|" /* IPv4 address */ + "\\[[0-9a-f:.]\\]" /* IPv6 address, sloppily */ + ")$", + G_REGEX_CASELESS | G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, &error); + + if (error != NULL) + g_error ("failed to compile the host regex: %s", error->message); + } + +#undef DOMAIN +#undef TLD + + return g_regex_match (host_regex, str, 0, NULL); +} + +static gboolean +priv_is_tel_num (const gchar *str) +{ + static GRegex *tel_num_regex = NULL; + + if (tel_num_regex == NULL) + { + GError *error = NULL; + + tel_num_regex = g_regex_new ( + "^\\s*[\\+(]?\\s*[0-9][-.0-9()\\s]*$", + G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, &error); + + if (error != NULL) + g_error ("failed to compile the telephone number regex: %s", + error->message); + } + + return g_regex_match (tel_num_regex, str, 0, NULL); +} + +/* Strip the non-essential characters from a string regarded as + * a telephone number */ +static gchar * +priv_strip_tel_num (const gchar *fuzzy) +{ + static GRegex *cruft_regex = NULL; + + if (cruft_regex == NULL) + { + GError *error = NULL; + + cruft_regex = g_regex_new ("[^+0-9]+", + G_REGEX_RAW | G_REGEX_OPTIMIZE, 0, &error); + + if (error != NULL) + g_error ("failed to compile the non-essential " + "telephone number cruft regex: %s", error->message); + } + + return g_regex_replace_literal (cruft_regex, fuzzy, -1, 0, "", 0, NULL); +} + +static const char * +priv_lowercase_url_part (su_home_t *home, const char *src) +{ + size_t n = 0; + size_t i; + char *res; + + for (i = 0; src[i]; i++) + { + if (g_ascii_isupper (src[i])) + { + n = i + strlen (src + i); + break; + } + } + + if (!src[i]) + return src; + + res = su_alloc (home, n + 1); + memcpy (res, src, i); + for (; i < n; i++) + res[i] = g_ascii_tolower (src[i]); + res[i] = '\0'; + + return (const char *) res; +} + +#define TPSIP_RESERVED_CHARS_ALLOWED_IN_USERNAME "!*'()&=+$,;?/" + +gchar * +rakia_normalize_contact (const gchar *sipuri, + const url_t *base_url, + const gchar *transport, + GError **error) +{ + su_home_t home[1] = { SU_HOME_INIT(home) }; + url_t *url; + gchar *retval = NULL; + char *c; + + url = url_make (home, sipuri); + + if (url == NULL || + (url->url_scheme == NULL && url->url_user == NULL)) + { + /* we got username or phone number, local to our domain */ + gchar *user; + + if (base_url == NULL || base_url->url_host == NULL) + { + WARNING ("bare name given, but no account URL is set"); + goto error; + } + + if (priv_is_tel_num (sipuri)) + { + user = priv_strip_tel_num (sipuri); + } + else + { + user = g_uri_escape_string (sipuri, + TPSIP_RESERVED_CHARS_ALLOWED_IN_USERNAME, FALSE); + } + + if (base_url->url_type == url_sips) + url = url_format (home, "sips:%s@%s", + user, base_url->url_host); + else + url = url_format (home, "sip:%s@%s", + user, base_url->url_host); + + g_free (user); + + if (!url) goto error; + } + else if (url->url_scheme == NULL) + { + /* Set the scheme to SIP or SIPS accordingly to the connection's + * transport preference */ + if (transport != NULL + && g_ascii_strcasecmp (transport, "tls") == 0) + { + url->url_type = url_sips; + url->url_scheme = "sips"; + } + else + { + url->url_type = url_sip; + url->url_scheme = "sip"; + } + } + + if (url_sanitize (url) != 0) goto error; + + /* scheme should've been set by now */ + if (url->url_scheme == NULL || (url->url_scheme[0] == 0)) + goto error; + + /* convert the scheme to lowercase */ + /* Note: we can't do it in place because url->url_scheme may point to + * a static string */ + url->url_scheme = priv_lowercase_url_part (home, url->url_scheme); + + /* Check that if we have '@', the username isn't empty. + * Note that we rely on Sofia-SIP to canonize the user name */ + if (url->url_user) + { + if (url->url_user[0] == 0) goto error; + } + + /* host should be set and valid */ + if (url->url_host == NULL || !priv_is_host (url->url_host)) + goto error; + + /* convert host to lowercase */ + for (c = (char *) url->url_host; *c; c++) + { + *c = g_ascii_tolower (*c); + } + + retval = g_strdup (url_as_string (home, url)); + +error: + if (retval == NULL) + g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_HANDLE, + "invalid SIP URI"); + + su_home_deinit (home); + return retval; +} diff --git a/rakia/handles.h b/rakia/handles.h new file mode 100644 index 0000000..914f167 --- /dev/null +++ b/rakia/handles.h @@ -0,0 +1,47 @@ +/* + * rakia/handle.h - Telepathy SIP handle management + * Copyright (C) 2011 Nokia Corporation + * @author Pekka Pessi <pekka.pessi@nokia.com> + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef TPSIP_HANDLE_H +#define TPSIP_HANDLE_H + +#include <telepathy-glib/base-connection.h> +#include <sofia-sip/sip.h> + +G_BEGIN_DECLS + +TpHandle rakia_handle_ensure (TpBaseConnection *, url_t const *, char const *); +TpHandle rakia_handle_by_requestor (TpBaseConnection *, sip_t const *sip); +void rakia_handle_unref (TpBaseConnection *, TpHandle handle); +char const *rakia_handle_inspect (TpBaseConnection *, TpHandle handle); +const url_t *rakia_handle_inspect_uri (TpBaseConnection *, TpHandle handle); + +gchar * rakia_handle_normalize (TpHandleRepoIface *repo, + const gchar *sipuri, + gpointer context, + GError **error); + +gchar *rakia_normalize_contact (const gchar *sipuri, + const url_t *base_url, + const gchar *transport, + GError **error); + +G_END_DECLS + +#endif /* !TPSIP_HANDLE_H */ diff --git a/rakia/media-channel.c b/rakia/media-channel.c new file mode 100644 index 0000000..089b2b9 --- /dev/null +++ b/rakia/media-channel.c @@ -0,0 +1,2096 @@ +/* + * sip-media-channel.c - Source for RakiaMediaChannel + * Copyright (C) 2005-2008 Collabora Ltd. + * Copyright (C) 2005-2010 Nokia Corporation + * @author Kai Vehmanen <first.surname@nokia.com> + * @author Mikhail Zabaluev <mikhail.zabaluev@nokia.com> + * + * Based on telepathy-gabble implementation (gabble-media-channel). + * @author Ole Andre Vadla Ravnaas <ole.andre.ravnaas@collabora.co.uk> + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include "rakia/media-channel.h" + +#include <stdlib.h> +#include <string.h> + +#include <telepathy-glib/channel-iface.h> +#include <telepathy-glib/dbus.h> +#include <telepathy-glib/errors.h> +#include <telepathy-glib/exportable-channel.h> +#include <telepathy-glib/interfaces.h> +#include <telepathy-glib/svc-channel.h> + +#include <rakia/event-target.h> + +#define DEBUG_FLAG TPSIP_DEBUG_MEDIA +#include "rakia/debug.h" + +#include <rakia/media-session.h> +#include <rakia/base-connection.h> + +#define TPSIP_CHANNEL_CALL_STATE_PROCEEDING_MASK \ + (TP_CHANNEL_CALL_STATE_RINGING | \ + TP_CHANNEL_CALL_STATE_QUEUED | \ + TP_CHANNEL_CALL_STATE_IN_PROGRESS) + +static void event_target_init (gpointer, gpointer); +static void channel_iface_init (gpointer, gpointer); +static void media_signalling_iface_init (gpointer, gpointer); +static void streamed_media_iface_init (gpointer, gpointer); +static void dtmf_iface_init (gpointer, gpointer); +static void call_state_iface_init (gpointer, gpointer); +static void hold_iface_init (gpointer, gpointer); + +G_DEFINE_TYPE_WITH_CODE (RakiaMediaChannel, rakia_media_channel, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (TPSIP_TYPE_EVENT_TARGET, event_target_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_DBUS_PROPERTIES, + tp_dbus_properties_mixin_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_PROPERTIES_INTERFACE, + tp_properties_mixin_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL, channel_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_INTERFACE_GROUP, + tp_group_mixin_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_INTERFACE_MEDIA_SIGNALLING, + media_signalling_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_INTERFACE_DTMF, + dtmf_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_INTERFACE_CALL_STATE, + call_state_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_INTERFACE_HOLD, + hold_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_TYPE_STREAMED_MEDIA, + streamed_media_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_EXPORTABLE_CHANNEL, NULL); + G_IMPLEMENT_INTERFACE (TP_TYPE_CHANNEL_IFACE, NULL)); + +static const gchar *rakia_media_channel_interfaces[] = { + TP_IFACE_CHANNEL_INTERFACE_GROUP, + TP_IFACE_CHANNEL_INTERFACE_MEDIA_SIGNALLING, + TP_IFACE_CHANNEL_INTERFACE_DTMF, + TP_IFACE_CHANNEL_INTERFACE_CALL_STATE, + TP_IFACE_CHANNEL_INTERFACE_HOLD, + TP_IFACE_PROPERTIES_INTERFACE, + NULL +}; + +/* properties */ +enum +{ + PROP_CONNECTION = 1, + PROP_OBJECT_PATH, + PROP_CHANNEL_TYPE, + PROP_HANDLE_TYPE, + PROP_HANDLE, + PROP_TARGET_ID, + PROP_INITIATOR, + PROP_INITIATOR_ID, + PROP_REQUESTED, + PROP_INTERFACES, + PROP_CHANNEL_DESTROYED, + PROP_CHANNEL_PROPERTIES, + PROP_INITIAL_AUDIO, + PROP_INITIAL_VIDEO, + PROP_IMMUTABLE_STREAMS, + /* Telepathy properties (see below too) */ + PROP_NAT_TRAVERSAL, + PROP_STUN_SERVER, + PROP_STUN_PORT, + LAST_PROPERTY +}; + +/* TP channel properties */ +enum +{ + TP_PROP_NAT_TRAVERSAL = 0, + TP_PROP_STUN_SERVER, + TP_PROP_STUN_PORT, + NUM_TP_PROPS +}; + +static const TpPropertySignature media_channel_property_signatures[NUM_TP_PROPS] = +{ + { "nat-traversal", G_TYPE_STRING }, + { "stun-server", G_TYPE_STRING }, + { "stun-port", G_TYPE_UINT }, +}; + +/* signals */ +enum +{ + SIG_INCOMING_CALL, + NUM_SIGNALS +}; + +static guint signals[NUM_SIGNALS] = { 0 }; + + +/* private structure */ +typedef struct _RakiaMediaChannelPrivate RakiaMediaChannelPrivate; + +struct _RakiaMediaChannelPrivate +{ + RakiaBaseConnection *conn; + RakiaMediaSession *session; + gchar *object_path; + TpHandle handle; + TpHandle initiator; + GHashTable *call_states; + gchar *stun_server; + guint stun_port; + + gboolean initial_audio; + gboolean initial_video; + gboolean immutable_streams; + gboolean closed; + gboolean dispose_has_run; +}; + +#define TPSIP_MEDIA_CHANNEL_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), TPSIP_TYPE_MEDIA_CHANNEL, RakiaMediaChannelPrivate)) + +/*********************************************************************** + * Set: Gobject interface + ***********************************************************************/ + +static void +rakia_media_channel_init (RakiaMediaChannel *self) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + /* allocate any data required by the object here */ + priv->call_states = g_hash_table_new (NULL, NULL); + + /* initialise the properties mixin *before* GObject + * sets the construct-time properties */ + tp_properties_mixin_init (G_OBJECT (self), + G_STRUCT_OFFSET (RakiaMediaChannel, properties)); +} + +static void +rakia_media_channel_constructed (GObject *obj) +{ + RakiaMediaChannel *chan = TPSIP_MEDIA_CHANNEL (obj); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (chan); + TpBaseConnection *conn = (TpBaseConnection *)(priv->conn); + GObjectClass *parent_object_class = + G_OBJECT_CLASS (rakia_media_channel_parent_class); + TpDBusDaemon *bus; + TpHandleRepoIface *contact_repo; + TpIntSet *add; + + if (parent_object_class->constructed != NULL) + parent_object_class->constructed (obj); + + contact_repo = tp_base_connection_get_handles (conn, + TP_HANDLE_TYPE_CONTACT); + + if (priv->handle != 0) + tp_handle_ref (contact_repo, priv->handle); + + /* register object on the bus */ + bus = tp_base_connection_get_dbus_daemon (conn); + + DEBUG("registering object to dbus path=%s", priv->object_path); + tp_dbus_daemon_register_object (bus, priv->object_path, obj); + + /* initialize group mixin */ + tp_group_mixin_init (obj, + G_STRUCT_OFFSET (RakiaMediaChannel, group), + contact_repo, + conn->self_handle); + + /* automatically add initiator to channel, but also ref them again (because + * priv->initiator is the InitiatorHandle) */ + g_assert (priv->initiator != 0); + tp_handle_ref (contact_repo, priv->initiator); + + add = tp_intset_new_containing (priv->initiator); + tp_group_mixin_change_members (obj, "", add, NULL, NULL, NULL, 0, 0); + tp_intset_destroy (add); + + /* We start off with lots of flags, and then delete them as we work out what + * kind of channel we are, rather than trying to track what we need to + * add/remove over time. We should always have the right flags before we are + * advertised on the bus. */ + tp_group_mixin_change_flags (obj, + TP_CHANNEL_GROUP_FLAG_CAN_ADD | TP_CHANNEL_GROUP_FLAG_CAN_REMOVE | + TP_CHANNEL_GROUP_FLAG_CAN_RESCIND | TP_CHANNEL_GROUP_FLAG_PROPERTIES, 0); +} + +static void rakia_media_channel_dispose (GObject *object); +static void rakia_media_channel_finalize (GObject *object); +static void rakia_media_channel_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec); +static void rakia_media_channel_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec); + +static void priv_create_session (RakiaMediaChannel *channel, + nua_handle_t *nh, + TpHandle peer); +static void priv_destroy_session(RakiaMediaChannel *channel); + +static void priv_outbound_call (RakiaMediaChannel *channel, + TpHandle peer); + +static gboolean rakia_media_channel_remove_with_reason ( + GObject *iface, + TpHandle handle, + const gchar *message, + guint reason, + GError **error); + +static void +rakia_media_channel_class_init (RakiaMediaChannelClass *klass) +{ + static TpDBusPropertiesMixinPropImpl channel_props[] = { + { "ChannelType", "channel-type", NULL }, + { "Interfaces", "interfaces", NULL }, + { "TargetHandleType", "handle-type", NULL }, + { "TargetHandle", "handle", NULL }, + { "TargetID", "target-id", NULL }, + { "InitiatorHandle", "initiator", NULL }, + { "InitiatorID", "initiator-id", NULL }, + { "Requested", "requested", NULL }, + { NULL } + }; + static TpDBusPropertiesMixinPropImpl streamed_media_props[] = { + { "InitialAudio", "initial-audio", NULL }, + { "InitialVideo", "initial-video", NULL }, + { "ImmutableStreams", "immutable-streams", NULL }, + { NULL } + }; + + static TpDBusPropertiesMixinIfaceImpl prop_interfaces[] = { + { TP_IFACE_CHANNEL, + tp_dbus_properties_mixin_getter_gobject_properties, + NULL, + channel_props, + }, + { TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA, + tp_dbus_properties_mixin_getter_gobject_properties, + NULL, + streamed_media_props, + }, + { NULL } + }; + + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GParamSpec *param_spec; + + DEBUG("enter"); + + g_type_class_add_private (klass, sizeof (RakiaMediaChannelPrivate)); + + object_class->constructed = rakia_media_channel_constructed; + object_class->dispose = rakia_media_channel_dispose; + object_class->finalize = rakia_media_channel_finalize; + + object_class->get_property = rakia_media_channel_get_property; + object_class->set_property = rakia_media_channel_set_property; + + g_object_class_override_property (object_class, PROP_HANDLE_TYPE, + "handle-type"); + g_object_class_override_property (object_class, PROP_HANDLE, "handle"); + g_object_class_override_property (object_class, PROP_OBJECT_PATH, + "object-path"); + g_object_class_override_property (object_class, PROP_CHANNEL_TYPE, + "channel-type"); + + g_object_class_override_property (object_class, PROP_CHANNEL_DESTROYED, + "channel-destroyed"); + g_object_class_override_property (object_class, PROP_CHANNEL_PROPERTIES, + "channel-properties"); + + param_spec = g_param_spec_object ("connection", "RakiaConnection object", + "SIP connection object that owns this SIP media channel object.", + TPSIP_TYPE_BASE_CONNECTION, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_CONNECTION, param_spec); + + param_spec = g_param_spec_string ("nat-traversal", "NAT traversal mechanism", + "A string representing the type of NAT traversal that should be " + "performed for streams on this channel.", + "none", + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_NAT_TRAVERSAL, param_spec); + + param_spec = g_param_spec_string ("stun-server", "STUN server", + "IP or address of STUN server.", NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_STUN_SERVER, param_spec); + + param_spec = g_param_spec_uint ("stun-port", "STUN port", + "UDP port of STUN server.", 0, G_MAXUINT16, 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_STUN_PORT, param_spec); + + param_spec = g_param_spec_boxed ("interfaces", "Extra D-Bus interfaces", + "Addition Channel.Interface.* interfaces", G_TYPE_STRV, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_INTERFACES, param_spec); + + param_spec = g_param_spec_string ("target-id", "Target SIP URI", + "Currently empty, because this channel always has handle 0.", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_TARGET_ID, param_spec); + + param_spec = g_param_spec_uint ("initiator", "Channel initiator", + "The TpHandle representing the contact who created the channel.", + 0, G_MAXUINT32, 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_INITIATOR, param_spec); + + param_spec = g_param_spec_string ("initiator-id", "Creator URI", + "The URI obtained by inspecting the initiator handle.", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_INITIATOR_ID, param_spec); + + param_spec = g_param_spec_boolean ("requested", "Requested?", + "True if this channel was requested by the local user", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_REQUESTED, param_spec); + + param_spec = g_param_spec_boolean ("initial-audio", "InitialAudio", + "Whether the channel initially contained an audio stream", + FALSE, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_INITIAL_AUDIO, + param_spec); + + param_spec = g_param_spec_boolean ("initial-video", "InitialVideo", + "Whether the channel initially contained a video stream", + FALSE, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_INITIAL_VIDEO, + param_spec); + + param_spec = g_param_spec_boolean ("immutable-streams", "ImmutableStreams", + "Whether the set of streams on this channel are fixed once requested", + FALSE, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_IMMUTABLE_STREAMS, + param_spec); + + signals[SIG_INCOMING_CALL] = + g_signal_new ("incoming-call", + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + tp_properties_mixin_class_init (object_class, + G_STRUCT_OFFSET (RakiaMediaChannelClass, properties_class), + media_channel_property_signatures, NUM_TP_PROPS, NULL); + + klass->dbus_props_class.interfaces = + prop_interfaces; + tp_dbus_properties_mixin_class_init (object_class, + G_STRUCT_OFFSET (RakiaMediaChannelClass, dbus_props_class)); + + tp_group_mixin_class_init (object_class, + G_STRUCT_OFFSET (RakiaMediaChannelClass, group_class), + _rakia_media_channel_add_member, + NULL); + tp_group_mixin_class_allow_self_removal (object_class); + tp_group_mixin_class_set_remove_with_reason_func(object_class, + rakia_media_channel_remove_with_reason); + tp_group_mixin_init_dbus_properties (object_class); + +} + +static void +rakia_media_channel_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + RakiaMediaChannel *chan = TPSIP_MEDIA_CHANNEL (object); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (chan); + TpBaseConnection *base_conn = TP_BASE_CONNECTION (priv->conn); + + switch (property_id) { + case PROP_CONNECTION: + g_value_set_object (value, priv->conn); + break; + case PROP_OBJECT_PATH: + g_value_set_string (value, priv->object_path); + break; + case PROP_CHANNEL_TYPE: + g_value_set_static_string (value, TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA); + break; + case PROP_HANDLE: + g_value_set_uint (value, priv->handle); + break; + case PROP_HANDLE_TYPE: + g_value_set_uint (value, priv->handle? + TP_HANDLE_TYPE_CONTACT : TP_HANDLE_TYPE_NONE); + break; + case PROP_TARGET_ID: + if (priv->handle != 0) + { + TpHandleRepoIface *repo = tp_base_connection_get_handles ( + base_conn, TP_HANDLE_TYPE_CONTACT); + + g_value_set_string (value, tp_handle_inspect (repo, priv->handle)); + } + else + g_value_set_static_string (value, ""); + break; + case PROP_INITIATOR: + g_value_set_uint (value, priv->initiator); + break; + case PROP_INITIATOR_ID: + { + TpHandleRepoIface *repo = tp_base_connection_get_handles ( + base_conn, TP_HANDLE_TYPE_CONTACT); + + g_value_set_string (value, tp_handle_inspect (repo, priv->initiator)); + } + break; + case PROP_REQUESTED: + g_value_set_boolean (value, (priv->initiator == base_conn->self_handle)); + break; + case PROP_INTERFACES: + g_value_set_static_boxed (value, rakia_media_channel_interfaces); + break; + case PROP_CHANNEL_DESTROYED: + g_value_set_boolean (value, priv->closed); + break; + case PROP_CHANNEL_PROPERTIES: + g_value_take_boxed (value, + tp_dbus_properties_mixin_make_properties_hash (object, + TP_IFACE_CHANNEL, "ChannelType", + TP_IFACE_CHANNEL, "TargetHandleType", + TP_IFACE_CHANNEL, "TargetHandle", + TP_IFACE_CHANNEL, "TargetID", + TP_IFACE_CHANNEL, "InitiatorHandle", + TP_IFACE_CHANNEL, "InitiatorID", + TP_IFACE_CHANNEL, "Requested", + TP_IFACE_CHANNEL, "Interfaces", + TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA, "InitialAudio", + TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA, "InitialVideo", + TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA, "ImmutableStreams", + NULL)); + break; + case PROP_INITIAL_AUDIO: + g_value_set_boolean (value, priv->initial_audio); + break; + case PROP_INITIAL_VIDEO: + g_value_set_boolean (value, priv->initial_video); + break; + case PROP_IMMUTABLE_STREAMS: + g_value_set_boolean (value, priv->immutable_streams); + break; + case PROP_STUN_SERVER: + g_value_set_string (value, priv->stun_server); + break; + case PROP_STUN_PORT: + g_value_set_uint (value, priv->stun_port); + break; + default: + /* Some properties live in the mixin */ + { + const gchar *param_name; + guint tp_property_id; + GValue *tp_property_value; + + param_name = g_param_spec_get_name (pspec); + if (G_LIKELY (tp_properties_mixin_has_property (object, param_name, + &tp_property_id))) + { + tp_property_value = + chan->properties.properties[tp_property_id].value; + + if (G_LIKELY (tp_property_value != NULL)) + { + g_value_copy (tp_property_value, value); + return; + } + } + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static gboolean +rakia_media_channel_set_tp_property (RakiaMediaChannel *chan, + const GValue *value, + GParamSpec *pspec) +{ + GObject *obj = (GObject *) chan; + const gchar *param_name = g_param_spec_get_name (pspec); + guint tp_property_id; + + if (G_LIKELY (tp_properties_mixin_has_property (obj, param_name, + &tp_property_id))) + { + tp_properties_mixin_change_value (obj, tp_property_id, + value, NULL); + tp_properties_mixin_change_flags (obj, tp_property_id, + TP_PROPERTY_FLAG_READ, 0, NULL); + return TRUE; + } + else + { + WARNING("Telepathy property '%s' is not defined for media channels", + param_name); + return FALSE; + } +} + +static void +rakia_media_channel_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + RakiaMediaChannel *chan = TPSIP_MEDIA_CHANNEL (object); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (chan); + + switch (property_id) { + case PROP_HANDLE_TYPE: + case PROP_CHANNEL_TYPE: + /* this property is writable in the interface, but not actually + * meaningfully changable on this channel, so we do nothing */ + break; + case PROP_CONNECTION: + priv->conn = g_value_dup_object (value); + break; + case PROP_OBJECT_PATH: + g_free (priv->object_path); + priv->object_path = g_value_dup_string (value); + break; + case PROP_HANDLE: + /* XXX: this property is defined as writable, + * but don't set it after construction, mmkay? */ + /* we don't ref it here because we don't necessarily have access to the + * contact repo yet - instead we ref it in constructed. */ + priv->handle = g_value_get_uint (value); + break; + case PROP_INITIATOR: + /* similarly we can't ref this yet */ + priv->initiator = g_value_get_uint (value); + break; + case PROP_INITIAL_AUDIO: + priv->initial_audio = g_value_get_boolean (value); + break; + case PROP_INITIAL_VIDEO: + priv->initial_video = g_value_get_boolean (value); + break; + case PROP_IMMUTABLE_STREAMS: + priv->immutable_streams = g_value_get_boolean (value); + break; + case PROP_STUN_SERVER: + priv->stun_server = g_value_dup_string (value); + /* Also expose as a legacy Telepathy property */ + rakia_media_channel_set_tp_property (chan, value, pspec); + break; + case PROP_STUN_PORT: + priv->stun_port = g_value_get_uint (value); + /* Also expose as a legacy Telepathy property */ + rakia_media_channel_set_tp_property (chan, value, pspec); + break; + default: + /* some properties live in the mixin */ + if (rakia_media_channel_set_tp_property (chan, value, pspec)) + return; + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +rakia_media_channel_dispose (GObject *object) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (object); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + TpHandleRepoIface *contact_handles; + + if (priv->dispose_has_run) + return; + + DEBUG("enter"); + + priv->dispose_has_run = TRUE; + + if (!priv->closed) + rakia_media_channel_close (self); + + contact_handles = tp_base_connection_get_handles ( + TP_BASE_CONNECTION (priv->conn), TP_HANDLE_TYPE_CONTACT); + + tp_handle_unref (contact_handles, priv->initiator); + priv->initiator = 0; + + g_object_unref (priv->conn); + + if (G_OBJECT_CLASS (rakia_media_channel_parent_class)->dispose) + G_OBJECT_CLASS (rakia_media_channel_parent_class)->dispose (object); + + DEBUG("exit"); +} + +static void +rakia_media_channel_finalize (GObject *object) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (object); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + g_hash_table_destroy (priv->call_states); + + g_free (priv->stun_server); + + g_free (priv->object_path); + + tp_group_mixin_finalize (object); + + tp_properties_mixin_finalize (object); + + G_OBJECT_CLASS (rakia_media_channel_parent_class)->finalize (object); + + DEBUG("exit"); +} + +/*********************************************************************** + * Set: Channel interface implementation (same for 0.12/0.13) + ***********************************************************************/ + +/** + * rakia_media_channel_close_async + * + * Implements DBus method Close + * on interface org.freedesktop.Telepathy.Channel + */ +static void +rakia_media_channel_dbus_close (TpSvcChannel *iface, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + + rakia_media_channel_close (self); + tp_svc_channel_return_from_close (context); +} + +void +rakia_media_channel_close (RakiaMediaChannel *obj) +{ + RakiaMediaChannelPrivate *priv; + + DEBUG("enter"); + + g_assert (TPSIP_IS_MEDIA_CHANNEL (obj)); + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (obj); + + if (priv->closed) + return; + + priv->closed = TRUE; + + if (priv->session) { + rakia_media_session_terminate (priv->session); + g_assert (priv->session == NULL); + } + + tp_svc_channel_emit_closed ((TpSvcChannel *)obj); + + return; +} + +/** + * rakia_media_channel_get_channel_type + * + * Implements DBus method GetChannelType + * on interface org.freedesktop.Telepathy.Channel + */ +static void +rakia_media_channel_get_channel_type (TpSvcChannel *obj, + DBusGMethodInvocation *context) +{ + tp_svc_channel_return_from_get_channel_type (context, + TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA); +} + + +/** + * rakia_media_channel_get_handle + * + * Implements DBus method GetHandle + * on interface org.freedesktop.Telepathy.Channel + */ +static void +rakia_media_channel_get_handle (TpSvcChannel *iface, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + if (priv->handle != 0) + tp_svc_channel_return_from_get_handle (context, TP_HANDLE_TYPE_CONTACT, + priv->handle); + else + tp_svc_channel_return_from_get_handle (context, TP_HANDLE_TYPE_NONE, 0); +} + +/** + * rakia_media_channel_get_interfaces + * + * Implements DBus method GetInterfaces + * on interface org.freedesktop.Telepathy.Channel + */ +static void +rakia_media_channel_get_interfaces (TpSvcChannel *iface, + DBusGMethodInvocation *context) +{ + tp_svc_channel_return_from_get_interfaces (context, + rakia_media_channel_interfaces); +} + +/*********************************************************************** + * Set: Channel.Interface.MediaSignalling Telepathy-0.13 interface + ***********************************************************************/ + +/** + * rakia_media_channel_get_session_handlers + * + * Implements DBus method GetSessionHandlers + * on interface org.freedesktop.Telepathy.Channel.Interface.MediaSignalling + * + * @error: Used to return a pointer to a GError detailing any error + * that occured, DBus will throw the error only if this + * function returns false. + * + * Returns: TRUE if successful, FALSE if an error was thrown. + */ +static void +rakia_media_channel_get_session_handlers (TpSvcChannelInterfaceMediaSignalling *iface, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv; + GPtrArray *ret; + GValue handler = { 0 }; + + DEBUG("enter"); + + g_assert (TPSIP_IS_MEDIA_CHANNEL (self)); + + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + ret = g_ptr_array_new (); + + if (priv->session) + { + GType handler_type; + gchar *path; + + g_object_get (priv->session, + "object-path", &path, + NULL); + + handler_type = dbus_g_type_get_struct ("GValueArray", + DBUS_TYPE_G_OBJECT_PATH, + G_TYPE_STRING, + G_TYPE_INVALID); + + g_value_init (&handler, handler_type); + g_value_take_boxed (&handler, + dbus_g_type_specialized_construct (handler_type)); + + dbus_g_type_struct_set (&handler, + 0, path, + 1, "rtp", + G_MAXUINT); + + g_free (path); + + g_ptr_array_add (ret, g_value_get_boxed (&handler)); + } + + tp_svc_channel_interface_media_signalling_return_from_get_session_handlers ( + context, ret); + + if (G_IS_VALUE(&handler)) + g_value_unset (&handler); + + g_ptr_array_free (ret, TRUE); +} + + +/*********************************************************************** + * Set: Channel.Type.StreamedMedia Telepathy-0.13 interface + ***********************************************************************/ + +/** + * rakia_media_channel_list_streams + * + * Implements D-Bus method ListStreams + * on interface org.freedesktop.Telepathy.Channel.Type.StreamedMedia + */ +static void +rakia_media_channel_list_streams (TpSvcChannelTypeStreamedMedia *iface, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv; + GPtrArray *ret = NULL; + + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + ret = g_ptr_array_new (); + + if (priv->session != NULL) + rakia_media_session_list_streams (priv->session, ret); + + tp_svc_channel_type_streamed_media_return_from_list_streams (context, ret); + + g_boxed_free (TP_ARRAY_TYPE_MEDIA_STREAM_INFO_LIST, ret); +} + +/** + * rakia_media_channel_remove_streams + * + * Implements D-Bus method RemoveStreams + * on interface org.freedesktop.Telepathy.Channel.Type.StreamedMedia + */ +static void +rakia_media_channel_remove_streams (TpSvcChannelTypeStreamedMedia *iface, + const GArray *streams, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv; + GError *error = NULL; + + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + if (priv->immutable_streams) + { + error = g_error_new (TP_ERRORS, TP_ERROR_NOT_IMPLEMENTED, + "Cannot remove streams from the existing channel"); + } + else if (priv->session != NULL) + { + rakia_media_session_remove_streams(priv->session, + streams, + &error); + } + else + { + error = g_error_new (TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "No session is available"); + } + + if (error != NULL) + { + dbus_g_method_return_error (context, error); + g_error_free (error); + return; + } + + tp_svc_channel_type_streamed_media_return_from_remove_streams (context); +} + +/** + * rakia_media_channel_request_stream_direction + * + * Implements D-Bus method RequestStreamDirection + * on interface org.freedesktop.Telepathy.Channel.Type.StreamedMedia + */ +static void +rakia_media_channel_request_stream_direction (TpSvcChannelTypeStreamedMedia *iface, + guint stream_id, + guint stream_direction, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv; + GError *error = NULL; + + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + if (priv->immutable_streams) + { + GError e = { TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "Cannot change directions on an immutable channel" }; + dbus_g_method_return_error (context, &e); + return; + } + + if (priv->session != NULL) + { + rakia_media_session_request_stream_direction (priv->session, + stream_id, + stream_direction, + &error); + } + else + { + error = g_error_new (TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "The media session is not available"); + } + + if (error == NULL) + { + tp_svc_channel_type_streamed_media_return_from_request_stream_direction (context); + } + else + { + dbus_g_method_return_error (context, error); + g_error_free (error); + } +} + + +/** + * rakia_media_channel_request_streams + * + * Implements D-Bus method RequestStreams + * on interface org.freedesktop.Telepathy.Channel.Type.StreamedMedia + */ +static void +rakia_media_channel_request_streams (TpSvcChannelTypeStreamedMedia *iface, + guint contact_handle, + const GArray *types, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + GError *error = NULL; + GPtrArray *ret = NULL; + RakiaMediaChannelPrivate *priv; + TpHandleRepoIface *contact_repo; + + DEBUG("enter"); + + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + if (priv->immutable_streams) + { + GError e = { TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "Cannot add streams to the immutable channel" }; + dbus_g_method_return_error (context, &e); + return; + } + + contact_repo = tp_base_connection_get_handles ( + (TpBaseConnection *)(priv->conn), TP_HANDLE_TYPE_CONTACT); + + if (!tp_handle_is_valid (contact_repo, contact_handle, &error)) + { + dbus_g_method_return_error (context, error); + g_error_free (error); + return; + } + + priv_outbound_call (self, contact_handle); + + ret = g_ptr_array_sized_new (types->len); + + if (rakia_media_session_request_streams (priv->session, types, ret, &error)) + { + g_assert (types->len == ret->len); + tp_svc_channel_type_streamed_media_return_from_request_streams (context, + ret); + } + else + { + dbus_g_method_return_error (context, error); + g_error_free (error); + } + + g_boxed_free (TP_ARRAY_TYPE_MEDIA_STREAM_INFO_LIST, ret); + + DEBUG ("exit"); +} + +/*********************************************************************** + * Set: sip-media-channel API towards sip-connection + ***********************************************************************/ + +void +rakia_media_channel_create_initial_streams (RakiaMediaChannel *self) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + g_assert (priv->initiator != priv->handle); + + /* RequestChannel(None, 0) => channel is anonymous: + * caller uses RequestStreams to set the peer and start the call. */ + if (priv->handle == 0) + return; + + priv_outbound_call (self, priv->handle); + + g_assert (priv->session != NULL); + + if (priv->initial_audio) + rakia_media_session_add_stream (priv->session, + TP_MEDIA_STREAM_TYPE_AUDIO, + TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL, + TRUE); + + if (priv->initial_video) + rakia_media_session_add_stream (priv->session, + TP_MEDIA_STREAM_TYPE_VIDEO, + TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL, + TRUE); +} + +/* + * Handles an incoming call, called shortly after the channel + * has been created with initiator handle of the sender, when remote SDP + * session data are reported by the NUA stack. + */ +static void +rakia_media_channel_handle_incoming_call (RakiaMediaChannel *self, + nua_handle_t *nh, + const sdp_session_t *sdp) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + TpBaseConnection *conn = TP_BASE_CONNECTION (priv->conn); + + g_assert (priv->initiator != conn->self_handle); + g_assert (priv->session == NULL); + + if (sdp != NULL) + { + /* Get the initial media properties from the session offer */ + const sdp_media_t *media; + + for (media = sdp->sdp_media; media != NULL; media = media->m_next) + { + if (media->m_rejected || media->m_port == 0) + continue; + + switch (media->m_type) + { + case sdp_media_audio: + priv->initial_audio = TRUE; + DEBUG("has initial audio"); + break; + case sdp_media_video: + priv->initial_video = TRUE; + DEBUG("has initial video"); + break; + default: + break; + } + } + } + + /* Tell the factory to emit NewChannel(s) */ + g_signal_emit (self, signals[SIG_INCOMING_CALL], 0); + + /* Offer the session handler to the client */ + priv_create_session (self, nh, priv->initiator); + + g_assert (priv->session != NULL); + rakia_media_session_receive_invite (priv->session); +} + +static gboolean +priv_nua_i_invite_cb (RakiaMediaChannel *self, + const RakiaNuaEvent *ev, + tagi_t tags[], + gpointer foo) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + /* nua_i_invite delivered for a bound handle means a re-INVITE */ + + g_return_val_if_fail (priv->session != NULL, FALSE); + + rakia_media_session_receive_reinvite (priv->session); + + return TRUE; +} + +static guint +rakia_media_channel_get_call_state (RakiaMediaChannel *self, + TpHandle peer) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + return GPOINTER_TO_UINT (g_hash_table_lookup (priv->call_states, + GUINT_TO_POINTER (peer))); +} + +static void +rakia_media_channel_peer_error (RakiaMediaChannel *self, + TpHandle peer, + guint status, + const char* message) +{ + TpGroupMixin *mixin = TP_GROUP_MIXIN (self); + TpIntSet *remove; + guint reason = TP_CHANNEL_GROUP_CHANGE_REASON_ERROR; + + switch (status) + { + case 410: + case 604: + reason = TP_CHANNEL_GROUP_CHANGE_REASON_INVALID_CONTACT; + break; + case 486: + case 600: + reason = TP_CHANNEL_GROUP_CHANGE_REASON_BUSY; + break; + case 408: + reason = TP_CHANNEL_GROUP_CHANGE_REASON_NO_ANSWER; + break; + case 404: + case 480: + reason = (rakia_media_channel_get_call_state (self, peer) + & TPSIP_CHANNEL_CALL_STATE_PROCEEDING_MASK) + ? TP_CHANNEL_GROUP_CHANGE_REASON_NO_ANSWER + : TP_CHANNEL_GROUP_CHANGE_REASON_OFFLINE; + break; + case 603: + /* No reason means roughly "rejected" */ + reason = TP_CHANNEL_GROUP_CHANGE_REASON_NONE; + break; + case 403: + case 401: + case 407: + reason = TP_CHANNEL_GROUP_CHANGE_REASON_PERMISSION_DENIED; + break; + } + + if (message == NULL || !g_utf8_validate (message, -1, NULL)) + message = ""; + + remove = tp_intset_new (); + tp_intset_add (remove, peer); + tp_intset_add (remove, mixin->self_handle); + tp_group_mixin_change_members ((GObject *)self, message, + NULL, remove, NULL, NULL, peer, reason); + tp_intset_destroy (remove); +} + +guint +rakia_media_channel_change_call_state (RakiaMediaChannel *self, + TpHandle peer, + guint flags_add, + guint flags_remove) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + gpointer key = GUINT_TO_POINTER (peer); + guint old_state; + guint new_state; + + /* XXX: check if the peer is a member? */ + + old_state = GPOINTER_TO_UINT (g_hash_table_lookup (priv->call_states, key)); + new_state = (old_state | flags_add) & ~flags_remove; + + if (new_state != old_state) + { + DEBUG ("setting call state %u for peer %u", new_state, peer); + if (new_state == 0) + g_hash_table_remove (priv->call_states, key); + else + g_hash_table_replace (priv->call_states, key, + GUINT_TO_POINTER (new_state)); + + tp_svc_channel_interface_call_state_emit_call_state_changed (self, + peer, + new_state); + } + + return new_state; +} + +static gboolean +priv_nua_i_bye_cb (RakiaMediaChannel *self, + const RakiaNuaEvent *ev, + tagi_t tags[], + gpointer foo) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + TpGroupMixin *mixin = TP_GROUP_MIXIN (self); + TpIntSet *remove; + TpHandle peer; + + g_return_val_if_fail (priv->session != NULL, FALSE); + + peer = rakia_media_session_get_peer (priv->session); + remove = tp_intset_new (); + tp_intset_add (remove, peer); + tp_intset_add (remove, mixin->self_handle); + + tp_group_mixin_change_members ((GObject *) self, "", + NULL, remove, NULL, NULL, + peer, TP_CHANNEL_GROUP_CHANGE_REASON_NONE); + + tp_intset_destroy (remove); + + return TRUE; +} + +static gboolean +priv_nua_i_cancel_cb (RakiaMediaChannel *self, + const RakiaNuaEvent *ev, + tagi_t tags[], + gpointer foo) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + TpGroupMixin *mixin = TP_GROUP_MIXIN (self); + TpIntSet *remove; + TpHandle actor = 0; + TpHandle peer; + const sip_reason_t *reason; + guint cause = 0; + const gchar *message = NULL; + + g_return_val_if_fail (priv->session != NULL, FALSE); + + /* FIXME: implement cancellation of an incoming re-INVITE, if ever + * found in real usage and not caused by a request timeout */ + + if (ev->sip != NULL) + for (reason = ev->sip->sip_reason; + reason != NULL; + reason = reason->re_next) + { + const char *protocol = reason->re_protocol; + if (protocol == NULL || strcmp (protocol, "SIP") != 0) + continue; + if (reason->re_cause != NULL) + { + cause = (guint) g_ascii_strtoull (reason->re_cause, NULL, 10); + message = reason->re_text; + break; + } + } + + peer = rakia_media_session_get_peer (priv->session); + + switch (cause) + { + case 200: + case 603: + /* The user must have acted on another branch of the forked call */ + actor = mixin->self_handle; + break; + default: + actor = peer; + } + + if (message == NULL || !g_utf8_validate (message, -1, NULL)) + message = ""; + + remove = tp_intset_new (); + tp_intset_add (remove, peer); + tp_intset_add (remove, mixin->self_handle); + + tp_group_mixin_change_members ((GObject *) self, message, + NULL, remove, NULL, NULL, + actor, TP_CHANNEL_GROUP_CHANGE_REASON_NONE); + + tp_intset_destroy (remove); + + return TRUE; +} + +static gboolean +priv_nua_i_state_cb (RakiaMediaChannel *self, + const RakiaNuaEvent *ev, + tagi_t tags[], + gpointer foo) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + const sdp_session_t *r_sdp = NULL; + int offer_recv = 0; + int answer_recv = 0; + int ss_state = nua_callstate_init; + gint status = ev->status; + TpHandle peer; + + tl_gets(tags, + NUTAG_CALLSTATE_REF(ss_state), + NUTAG_OFFER_RECV_REF(offer_recv), + NUTAG_ANSWER_RECV_REF(answer_recv), + SOATAG_REMOTE_SDP_REF(r_sdp), + TAG_END()); + + DEBUG("call with handle %p is %s", ev->nua_handle, nua_callstate_name (ss_state)); + + if (ss_state == nua_callstate_received && priv->session == NULL) + { + /* We get the session data for initial media properties with this event; + * initialize the session before we can create any streams below. + */ + rakia_media_channel_handle_incoming_call (self, ev->nua_handle, r_sdp); + } + + g_return_val_if_fail (priv->session != NULL, FALSE); + + if (r_sdp) + { + g_return_val_if_fail (answer_recv || offer_recv, FALSE); + if (!rakia_media_session_set_remote_media (priv->session, r_sdp)) + { + rakia_media_channel_close (self); + return TRUE; + } + } + + peer = rakia_media_session_get_peer (priv->session); + + switch ((enum nua_callstate)ss_state) + { + case nua_callstate_proceeding: + switch (status) + { + case 180: + rakia_media_channel_change_call_state (self, peer, + TP_CHANNEL_CALL_STATE_RINGING, 0); + break; + case 182: + rakia_media_channel_change_call_state (self, peer, + TP_CHANNEL_CALL_STATE_QUEUED, 0); + break; + case 183: + rakia_media_channel_change_call_state (self, peer, + TP_CHANNEL_CALL_STATE_IN_PROGRESS, 0); + break; + } + break; + + case nua_callstate_completing: + /* In auto-ack mode, we don't need to call nua_ack(), see NUTAG_AUTOACK() */ + break; + + case nua_callstate_ready: + + /* Clear any pre-establishment call states */ + rakia_media_channel_change_call_state (self, peer, 0, + TPSIP_CHANNEL_CALL_STATE_PROCEEDING_MASK); + + if (status < 300) + { + TpIntSet *add = tp_intset_new_containing (peer); + + tp_group_mixin_change_members ((GObject *) self, + "", + add, /* add */ + NULL, /* remove */ + NULL, + NULL, + peer, + TP_CHANNEL_GROUP_CHANGE_REASON_NONE); + + tp_intset_destroy (add); + + rakia_media_session_accept (priv->session); + } + else if (status == 491) + rakia_media_session_resolve_glare (priv->session); + else + { + /* Was something wrong with our re-INVITE? We can't cope anyway. */ + MESSAGE ("can't handle non-fatal response %d %s", status, ev->text); + rakia_media_session_terminate (priv->session); + } + break; + + case nua_callstate_terminated: + /* In cases of self-inflicted termination, + * we should have already gone through the moves */ + if (rakia_media_session_get_state (priv->session) + == TPSIP_MEDIA_SESSION_STATE_ENDED) + break; + + if (status >= 300) + { + rakia_media_channel_peer_error ( + self, peer, status, ev->text); + } + + rakia_media_session_change_state (priv->session, + TPSIP_MEDIA_SESSION_STATE_ENDED); + break; + + default: + break; + } + + return TRUE; +} + +static void priv_session_state_changed_cb (RakiaMediaSession *session, + guint old_state, + guint state, + RakiaMediaChannel *channel) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (channel); + TpGroupMixin *mixin = TP_GROUP_MIXIN (channel); + TpHandle self_handle; + TpHandle peer; + TpIntSet *set = NULL; + + DEBUG("enter"); + + self_handle = mixin->self_handle; + peer = rakia_media_session_get_peer (session); + + switch (state) + { + case TPSIP_MEDIA_SESSION_STATE_INVITE_SENT: + g_assert (priv->initiator == self_handle); + + /* add the peer to remote pending */ + set = tp_intset_new_containing (peer); + tp_group_mixin_change_members ((GObject *)channel, + "", + NULL, /* add */ + NULL, /* remove */ + NULL, /* local pending */ + set, /* remote pending */ + self_handle, /* actor */ + TP_CHANNEL_GROUP_CHANGE_REASON_INVITED); + + /* update flags: no more adding */ + tp_group_mixin_change_flags ((GObject *)channel, 0, + TP_CHANNEL_GROUP_FLAG_CAN_ADD); + + break; + + case TPSIP_MEDIA_SESSION_STATE_INVITE_RECEIVED: + /* add ourself to local pending */ + set = tp_intset_new_containing (self_handle); + tp_group_mixin_change_members ((GObject *) channel, "", + NULL, /* add */ + NULL, /* remove */ + set, /* local pending */ + NULL, /* remote pending */ + priv->initiator, /* actor */ + TP_CHANNEL_GROUP_CHANGE_REASON_INVITED); + + /* No adding more members to the incoming call. Therefore also not + * possible to add anyone to remote-pending, so rescinding would make + * utterly no sense. We also disallow removing the remote peer if + * we are not the initiator, so disallow that too. + * Removing yourself to end the call is not represented by group flags. + */ + tp_group_mixin_change_flags ((GObject *) channel, 0, + TP_CHANNEL_GROUP_FLAG_CAN_ADD | TP_CHANNEL_GROUP_FLAG_CAN_REMOVE | + TP_CHANNEL_GROUP_FLAG_CAN_RESCIND); + + break; + + case TPSIP_MEDIA_SESSION_STATE_ACTIVE: + if (priv->initiator == self_handle) + { + if (!tp_handle_set_is_member (mixin->remote_pending, peer)) + break; /* no-op */ + + /* the peer has promoted itself to members */ + set = tp_intset_new_containing (peer); + tp_group_mixin_change_members ((GObject *)channel, "", + set, /* add */ + NULL, /* remove */ + NULL, + NULL, + peer, 0); + } + else + { + if (!tp_handle_set_is_member (mixin->local_pending, self_handle)) + break; /* no-op */ + + /* promote ourselves to members */ + set = tp_intset_new_containing (self_handle); + tp_group_mixin_change_members ((GObject *)channel, "", + set, /* add */ + NULL, /* remove */ + NULL, + NULL, + self_handle, 0); + } + + /* update flags: deny adding and rescinding. Removing the remote peer is + * still allowed. + * Removing yourself to end the call is not represented by group flags. + */ + tp_group_mixin_change_flags ((GObject *)channel, 0, + TP_CHANNEL_GROUP_FLAG_CAN_ADD | TP_CHANNEL_GROUP_FLAG_CAN_RESCIND); + + break; + + case TPSIP_MEDIA_SESSION_STATE_ENDED: + set = tp_intset_new (); + + /* remove us and the peer from the member list */ + tp_intset_add (set, self_handle); + tp_intset_add (set, peer); + tp_group_mixin_change_members ((GObject *)channel, "", + NULL, /* add */ + set, /* remove */ + NULL, + NULL, + 0, 0); + + /* Close the channel; destroy the session first to avoid + * the rakia_media_session_terminate() path in this case */ + priv_destroy_session (channel); + rakia_media_channel_close (channel); + break; + } + + if (set != NULL) + tp_intset_destroy (set); +} + +void +rakia_media_channel_attach_to_nua_handle (RakiaMediaChannel *self, + nua_handle_t *nh) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + rakia_event_target_attach (nh, (GObject *) self); + + /* have the connection handle authentication, before all other + * response callbacks */ + rakia_base_connection_add_auth_handler (priv->conn, TPSIP_EVENT_TARGET (self)); + + g_signal_connect (self, + "nua-event::nua_i_invite", + G_CALLBACK (priv_nua_i_invite_cb), + NULL); + g_signal_connect (self, + "nua-event::nua_i_bye", + G_CALLBACK (priv_nua_i_bye_cb), + NULL); + g_signal_connect (self, + "nua-event::nua_i_cancel", + G_CALLBACK (priv_nua_i_cancel_cb), + NULL); + g_signal_connect (self, + "nua-event::nua_i_state", + G_CALLBACK (priv_nua_i_state_cb), + NULL); + +} + +/** + * priv_create_session: + * + * Creates a RakiaMediaSession object for given peer. + **/ +static void +priv_create_session (RakiaMediaChannel *channel, + nua_handle_t *nh, + TpHandle peer) +{ + RakiaMediaChannelPrivate *priv; + RakiaMediaSession *session; + TpBaseConnection *conn; + TpHandleRepoIface *contact_repo; + gchar *object_path; + gchar *local_ip_address = NULL; + + DEBUG("enter"); + + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (channel); + conn = (TpBaseConnection *)(priv->conn); + contact_repo = tp_base_connection_get_handles (conn, + TP_HANDLE_TYPE_CONTACT); + + g_assert (priv->session == NULL); + + object_path = g_strdup_printf ("%s/MediaSession%u", priv->object_path, peer); + + DEBUG("allocating session, peer=%u", peer); + + /* The channel manages references to the peer handle for the session */ + tp_handle_ref (contact_repo, peer); + + g_object_get (priv->conn, + "local-ip-address", &local_ip_address, + NULL); + + session = g_object_new (TPSIP_TYPE_MEDIA_SESSION, + "dbus-daemon", + tp_base_connection_get_dbus_daemon (conn), + "media-channel", channel, + "object-path", object_path, + "nua-handle", nh, + "peer", peer, + "local-ip-address", local_ip_address, + NULL); + + g_free (local_ip_address); + + g_signal_connect_object (session, + "state-changed", + G_CALLBACK(priv_session_state_changed_cb), + channel, + 0); + + priv->session = session; + + tp_svc_channel_interface_media_signalling_emit_new_session_handler ( + (TpSvcChannelInterfaceMediaSignalling *)channel, object_path, "rtp"); + + g_free (object_path); + + DEBUG ("exit"); +} + +static void +priv_destroy_session(RakiaMediaChannel *channel) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (channel); + RakiaMediaSession *session; + TpBaseConnection *conn; + TpHandleRepoIface *contact_repo; + + session = priv->session; + if (session == NULL) + return; + + DEBUG("enter"); + + /* Release the peer handle */ + conn = (TpBaseConnection *)(priv->conn); + contact_repo = tp_base_connection_get_handles (conn, + TP_HANDLE_TYPE_CONTACT); + tp_handle_unref (contact_repo, rakia_media_session_get_peer (session)); + + priv->session = NULL; + g_object_unref (session); + + DEBUG("exit"); +} + +/* + * Creates an outbound call session if a session does not exist + */ +static void +priv_outbound_call (RakiaMediaChannel *channel, + TpHandle peer) +{ + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (channel); + nua_handle_t *nh; + + if (priv->session == NULL) + { + DEBUG("making outbound call - setting peer handle to %u", peer); + + nh = rakia_base_connection_create_handle (priv->conn, peer); + priv_create_session (channel, nh, peer); + + /* Bind the channel object to the handle to handle NUA events */ + rakia_media_channel_attach_to_nua_handle (channel, nh); + + nua_handle_unref (nh); + } + else + DEBUG("session already exists"); + + g_assert (priv->session != NULL); +} + +gboolean +_rakia_media_channel_add_member (GObject *iface, + TpHandle handle, + const gchar *message, + GError **error) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + TpGroupMixin *mixin = TP_GROUP_MIXIN (iface); + + DEBUG("mixin->self_handle=%d, handle=%d", mixin->self_handle, handle); + + if (priv->initiator == mixin->self_handle) + { + TpIntSet *remote_pending; + + /* case a: an old-school outbound call + * (we are the initiator, a new handle added with AddMembers) */ + + priv_outbound_call (self, handle); + + /* Backwards compatible behavior: + * add the peer to remote pending without waiting for the actual request + * to be sent */ + remote_pending = tp_intset_new_containing (handle); + tp_group_mixin_change_members (iface, + "", + NULL, /* add */ + NULL, /* remove */ + NULL, /* local pending */ + remote_pending, /* remote pending */ + mixin->self_handle, /* actor */ + TP_CHANNEL_GROUP_CHANGE_REASON_INVITED); + tp_intset_destroy (remote_pending); + + /* update flags: no more adding. + * Removal and rescinding are still allowed. */ + tp_group_mixin_change_flags (iface, 0, + TP_CHANNEL_GROUP_FLAG_CAN_ADD); + + return TRUE; + } + if (priv->session && + handle == mixin->self_handle && + tp_handle_set_is_member (mixin->local_pending, handle)) + { + /* case b: an incoming invite */ + DEBUG("accepting an incoming invite"); + g_return_val_if_fail (priv->session != NULL, FALSE); + + rakia_media_session_accept (priv->session); + + return TRUE; + } + + MESSAGE ("unsupported member change requested for a media channel"); + + g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "handle %u cannot be added in the current state", handle); + return FALSE; +} + +static gint +rakia_status_from_tp_reason (TpChannelGroupChangeReason reason) +{ + switch (reason) + { + case TP_CHANNEL_GROUP_CHANGE_REASON_NONE: + return 603; /* Decline */ + case TP_CHANNEL_GROUP_CHANGE_REASON_NO_ANSWER: + case TP_CHANNEL_GROUP_CHANGE_REASON_OFFLINE: + return 480; /* Temporarily Unavailable */ + case TP_CHANNEL_GROUP_CHANGE_REASON_BUSY: + return 486; /* Busy Here */ + case TP_CHANNEL_GROUP_CHANGE_REASON_PERMISSION_DENIED: + case TP_CHANNEL_GROUP_CHANGE_REASON_BANNED: + return 403; /* Forbidden */ + case TP_CHANNEL_GROUP_CHANGE_REASON_INVALID_CONTACT: + return 404; /* Not Found */ + default: + return 500; /* Server Internal Error */ + } +} + +static gboolean +rakia_media_channel_remove_with_reason (GObject *obj, + TpHandle handle, + const gchar *message, + guint reason, + GError **error) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (obj); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + TpGroupMixin *mixin = TP_GROUP_MIXIN (obj); + TpIntSet *set = NULL; + TpHandle self_handle; + gboolean rejected; + + self_handle = mixin->self_handle; + + if (priv->initiator != self_handle && handle != self_handle) + { + g_set_error (error, TP_ERRORS, TP_ERROR_PERMISSION_DENIED, + "handle %u cannot be removed because you are not the initiator of the" + " channel", handle); + + return FALSE; + } + + if (priv->session == NULL) + { + g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "handle %u cannot be removed in the current state", handle); + + return FALSE; + } + + rejected = (handle == self_handle + && tp_handle_set_is_member (mixin->local_pending, handle)); + + /* We have excluded all the problem cases. + * Now we always want to remove both members on behalf of the local user */ + set = tp_intset_new (); + tp_intset_add (set, self_handle); + tp_intset_add (set, rakia_media_session_get_peer (priv->session)); + tp_group_mixin_change_members (obj, "", + NULL, /* add */ + set, /* remove */ + NULL, + NULL, + self_handle, 0); + tp_intset_destroy (set); + + if (rejected) + { + /* The user has rejected the call */ + + gint status; + + status = rakia_status_from_tp_reason (reason); + + /* XXX: raise NotAvailable if it's the wrong state? */ + rakia_media_session_respond (priv->session, status, message); + + /* This session is effectively ended, prevent the nua_i_state handler + * from useless work */ + rakia_media_session_change_state (priv->session, + TPSIP_MEDIA_SESSION_STATE_ENDED); + } + else + { + /* Want to terminate the call in whatever other situation; + * rescinding is handled by sending CANCEL */ + rakia_media_session_terminate (priv->session); + } + + return TRUE; +} + +static void +rakia_media_channel_get_call_states (TpSvcChannelInterfaceCallState *iface, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + tp_svc_channel_interface_call_state_return_from_get_call_states ( + context, + priv->call_states); +} + +static void +rakia_media_channel_get_hold_state (TpSvcChannelInterfaceHold *iface, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + TpLocalHoldState hold_state = TP_LOCAL_HOLD_STATE_UNHELD; + TpLocalHoldStateReason hold_reason = TP_LOCAL_HOLD_STATE_REASON_NONE; + + if (priv->session == NULL) + { + GError e = {TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "The media session is not available"}; + dbus_g_method_return_error (context, &e); + } + + g_object_get (priv->session, + "hold-state", &hold_state, + "hold-state-reason", &hold_reason, + NULL); + + tp_svc_channel_interface_hold_return_from_get_hold_state (context, + hold_state, + hold_reason); +} + +static void +rakia_media_channel_request_hold (TpSvcChannelInterfaceHold *iface, + gboolean hold, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv; + + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + if (priv->immutable_streams) + { + GError e = {TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "Session modification disabled"}; + dbus_g_method_return_error (context, &e); + return; + } + else if (priv->session != NULL) + { + rakia_media_session_request_hold (priv->session, hold); + } + else + { + GError e = {TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "The media session is not available"}; + dbus_g_method_return_error (context, &e); + return; + } + + tp_svc_channel_interface_hold_return_from_request_hold (context); +} + +static void +rakia_media_channel_start_tone (TpSvcChannelInterfaceDTMF *iface, + guint stream_id, + guchar event, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv; + GError *error = NULL; + + DEBUG("enter"); + + g_assert (TPSIP_IS_MEDIA_CHANNEL (self)); + + if (event >= NUM_TP_DTMF_EVENTS) + { + g_set_error (&error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "event %u is not a known DTMF event", event); + dbus_g_method_return_error (context, error); + g_error_free (error); + return; + } + + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + if (!rakia_media_session_start_telephony_event (priv->session, + stream_id, + event, + &error)) + { + dbus_g_method_return_error (context, error); + g_error_free (error); + return; + } + + tp_svc_channel_interface_dtmf_return_from_start_tone (context); +} + +static void +rakia_media_channel_stop_tone (TpSvcChannelInterfaceDTMF *iface, + guint stream_id, + DBusGMethodInvocation *context) +{ + RakiaMediaChannel *self = TPSIP_MEDIA_CHANNEL (iface); + RakiaMediaChannelPrivate *priv; + GError *error = NULL; + + DEBUG("enter"); + + g_assert (TPSIP_IS_MEDIA_CHANNEL (self)); + + priv = TPSIP_MEDIA_CHANNEL_GET_PRIVATE (self); + + if (!rakia_media_session_stop_telephony_event (priv->session, + stream_id, + &error)) + { + dbus_g_method_return_error (context, error); + g_error_free (error); + return; + } + + tp_svc_channel_interface_dtmf_return_from_stop_tone (context); +} + +static void +event_target_init(gpointer g_iface, gpointer iface_data) +{ +} + +static void +channel_iface_init(gpointer g_iface, gpointer iface_data) +{ + TpSvcChannelClass *klass = (TpSvcChannelClass *)g_iface; + + tp_svc_channel_implement_close ( + klass, rakia_media_channel_dbus_close); +#define IMPLEMENT(x) tp_svc_channel_implement_##x (\ + klass, rakia_media_channel_##x) + IMPLEMENT(get_channel_type); + IMPLEMENT(get_handle); + IMPLEMENT(get_interfaces); +#undef IMPLEMENT +} + +static void +streamed_media_iface_init(gpointer g_iface, gpointer iface_data) +{ + TpSvcChannelTypeStreamedMediaClass *klass = (TpSvcChannelTypeStreamedMediaClass *)g_iface; + +#define IMPLEMENT(x) tp_svc_channel_type_streamed_media_implement_##x (\ + klass, rakia_media_channel_##x) + IMPLEMENT(list_streams); + IMPLEMENT(remove_streams); + IMPLEMENT(request_stream_direction); + IMPLEMENT(request_streams); +#undef IMPLEMENT +} + +static void +media_signalling_iface_init(gpointer g_iface, gpointer iface_data) +{ + TpSvcChannelInterfaceMediaSignallingClass *klass = (TpSvcChannelInterfaceMediaSignallingClass *)g_iface; + +#define IMPLEMENT(x) tp_svc_channel_interface_media_signalling_implement_##x (\ + klass, rakia_media_channel_##x) + IMPLEMENT(get_session_handlers); +#undef IMPLEMENT +} + +static void +dtmf_iface_init (gpointer g_iface, gpointer iface_data) +{ + TpSvcChannelInterfaceDTMFClass *klass = (TpSvcChannelInterfaceDTMFClass *)g_iface; + +#define IMPLEMENT(x) tp_svc_channel_interface_dtmf_implement_##x (\ + klass, rakia_media_channel_##x) + IMPLEMENT(start_tone); + IMPLEMENT(stop_tone); +#undef IMPLEMENT +} + +static void +call_state_iface_init (gpointer g_iface, + gpointer iface_data) +{ + TpSvcChannelInterfaceCallStateClass *klass = g_iface; +#define IMPLEMENT(x) tp_svc_channel_interface_call_state_implement_##x (\ + klass, rakia_media_channel_##x) + IMPLEMENT (get_call_states); +#undef IMPLEMENT +} + +static void +hold_iface_init (gpointer g_iface, + gpointer iface_data) +{ + TpSvcChannelInterfaceHoldClass *klass = g_iface; + +#define IMPLEMENT(x) tp_svc_channel_interface_hold_implement_##x (\ + klass, rakia_media_channel_##x) + IMPLEMENT (get_hold_state); + IMPLEMENT (request_hold); +#undef IMPLEMENT +} diff --git a/rakia/media-channel.h b/rakia/media-channel.h new file mode 100644 index 0000000..6b8b07d --- /dev/null +++ b/rakia/media-channel.h @@ -0,0 +1,92 @@ +/* + * sip-media-channel.h - Header for RakiaMediaChannel + * Copyright (C) 2005 Collabora Ltd. + * Copyright (C) 2005-2009 Nokia Corporation + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __TPSIP_MEDIA_CHANNEL_H__ +#define __TPSIP_MEDIA_CHANNEL_H__ + +#include <glib-object.h> +#include <sofia-sip/sdp.h> +#include <telepathy-glib/dbus-properties-mixin.h> +#include <telepathy-glib/group-mixin.h> +#include <telepathy-glib/handle.h> +#include <telepathy-glib/properties-mixin.h> + +#include <rakia/sofia-decls.h> + + +G_BEGIN_DECLS + +typedef struct _RakiaMediaChannel RakiaMediaChannel; +typedef struct _RakiaMediaChannelClass RakiaMediaChannelClass; + +struct _RakiaMediaChannelClass { + GObjectClass parent_class; + TpGroupMixinClass group_class; + TpPropertiesMixinClass properties_class; + TpDBusPropertiesMixinClass dbus_props_class; +}; + +struct _RakiaMediaChannel { + GObject parent; + TpGroupMixin group; + TpPropertiesMixin properties; +}; + +GType rakia_media_channel_get_type(void); + +/* TYPE MACROS */ +#define TPSIP_TYPE_MEDIA_CHANNEL \ + (rakia_media_channel_get_type()) +#define TPSIP_MEDIA_CHANNEL(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), TPSIP_TYPE_MEDIA_CHANNEL, RakiaMediaChannel)) +#define TPSIP_MEDIA_CHANNEL_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), TPSIP_TYPE_MEDIA_CHANNEL, RakiaMediaChannelClass)) +#define TPSIP_IS_MEDIA_CHANNEL(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), TPSIP_TYPE_MEDIA_CHANNEL)) +#define TPSIP_IS_MEDIA_CHANNEL_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), TPSIP_TYPE_MEDIA_CHANNEL)) +#define TPSIP_MEDIA_CHANNEL_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), TPSIP_TYPE_MEDIA_CHANNEL, RakiaMediaChannelClass)) + +/*********************************************************************** + * Additional declarations (not based on generated templates) + ***********************************************************************/ + +void rakia_media_channel_close (RakiaMediaChannel *self); + +gboolean _rakia_media_channel_add_member (GObject *iface, + TpHandle handle, + const gchar *message, + GError **error); + +void rakia_media_channel_create_initial_streams (RakiaMediaChannel *self); + +void rakia_media_channel_attach_to_nua_handle (RakiaMediaChannel *self, + nua_handle_t *nh); + +guint +rakia_media_channel_change_call_state (RakiaMediaChannel *self, + TpHandle peer, + guint flags_add, + guint flags_remove); + +G_END_DECLS + +#endif /* #ifndef __TPSIP_MEDIA_CHANNEL_H__*/ diff --git a/rakia/media-manager.c b/rakia/media-manager.c new file mode 100644 index 0000000..57dd807 --- /dev/null +++ b/rakia/media-manager.c @@ -0,0 +1,701 @@ +/* + * media-manager.c - Media channel manager for SIP connection manager + * Copyright (C) 2007-2008 Collabora Ltd. + * Copyright (C) 2007-2010 Nokia Corporation + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include "rakia/media-manager.h" + +#include <string.h> + +#include <telepathy-glib/channel-manager.h> +#include <telepathy-glib/dbus.h> +#include <telepathy-glib/interfaces.h> + +#include "rakia/media-channel.h" +#include "rakia/base-connection.h" +#include "rakia/handles.h" + +#include <sofia-sip/sip_status.h> + +#define DEBUG_FLAG TPSIP_DEBUG_CONNECTION +#include "rakia/debug.h" + +typedef enum { + TPSIP_MEDIA_CHANNEL_CREATE_WITH_AUDIO = 1 << 0, + TPSIP_MEDIA_CHANNEL_CREATE_WITH_VIDEO = 1 << 1, +} RakiaMediaChannelCreationFlags; + +static void channel_manager_iface_init (gpointer, gpointer); +static void rakia_media_manager_constructed (GObject *object); +static void rakia_media_manager_close_all (RakiaMediaManager *fac); + +G_DEFINE_TYPE_WITH_CODE (RakiaMediaManager, rakia_media_manager, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (TP_TYPE_CHANNEL_MANAGER, + channel_manager_iface_init)) + +enum +{ + PROP_CONNECTION = 1, + PROP_STUN_SERVER, + PROP_STUN_PORT, + LAST_PROPERTY +}; + +typedef struct _RakiaMediaManagerPrivate RakiaMediaManagerPrivate; +struct _RakiaMediaManagerPrivate +{ + /* unreferenced (since it owns this manager) */ + TpBaseConnection *conn; + /* array of referenced (RakiaMediaChannel *) */ + GPtrArray *channels; + /* for unique channel object paths, currently always increments */ + guint channel_index; + + gulong status_changed_id; + gulong invite_received_id; + + gchar *stun_server; + guint16 stun_port; + + gboolean dispose_has_run; +}; + +#define TPSIP_MEDIA_MANAGER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), TPSIP_TYPE_MEDIA_MANAGER, RakiaMediaManagerPrivate)) + +static void +rakia_media_manager_init (RakiaMediaManager *fac) +{ + RakiaMediaManagerPrivate *priv = TPSIP_MEDIA_MANAGER_GET_PRIVATE (fac); + + priv->conn = NULL; + priv->channels = g_ptr_array_sized_new (1); + priv->channel_index = 0; + priv->dispose_has_run = FALSE; +} + +static void +rakia_media_manager_dispose (GObject *object) +{ + RakiaMediaManager *fac = TPSIP_MEDIA_MANAGER (object); + RakiaMediaManagerPrivate *priv = TPSIP_MEDIA_MANAGER_GET_PRIVATE (fac); + + if (priv->dispose_has_run) + return; + + priv->dispose_has_run = TRUE; + + rakia_media_manager_close_all (fac); + g_assert (priv->channels == NULL); + + if (G_OBJECT_CLASS (rakia_media_manager_parent_class)->dispose) + G_OBJECT_CLASS (rakia_media_manager_parent_class)->dispose (object); +} + +static void +rakia_media_manager_finalize (GObject *object) +{ + RakiaMediaManager *fac = TPSIP_MEDIA_MANAGER (object); + RakiaMediaManagerPrivate *priv = TPSIP_MEDIA_MANAGER_GET_PRIVATE (fac); + + g_free (priv->stun_server); +} + +static void +rakia_media_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + RakiaMediaManager *fac = TPSIP_MEDIA_MANAGER (object); + RakiaMediaManagerPrivate *priv = TPSIP_MEDIA_MANAGER_GET_PRIVATE (fac); + + switch (property_id) { + case PROP_CONNECTION: + g_value_set_object (value, priv->conn); + break; + case PROP_STUN_SERVER: + g_value_set_string (value, priv->stun_server); + break; + case PROP_STUN_PORT: + g_value_set_uint (value, priv->stun_port); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +rakia_media_manager_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + RakiaMediaManager *fac = TPSIP_MEDIA_MANAGER (object); + RakiaMediaManagerPrivate *priv = TPSIP_MEDIA_MANAGER_GET_PRIVATE (fac); + + switch (property_id) { + case PROP_CONNECTION: + priv->conn = g_value_get_object (value); + break; + case PROP_STUN_SERVER: + g_free (priv->stun_server); + priv->stun_server = g_value_dup_string (value); + break; + case PROP_STUN_PORT: + priv->stun_port = g_value_get_uint (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +rakia_media_manager_class_init (RakiaMediaManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GParamSpec *param_spec; + + g_type_class_add_private (klass, sizeof (RakiaMediaManagerPrivate)); + + object_class->constructed = rakia_media_manager_constructed; + object_class->get_property = rakia_media_manager_get_property; + object_class->set_property = rakia_media_manager_set_property; + object_class->dispose = rakia_media_manager_dispose; + object_class->finalize = rakia_media_manager_finalize; + + param_spec = g_param_spec_object ("connection", + "RakiaBaseConnection object", + "SIP connection that owns this media channel manager", + TPSIP_TYPE_BASE_CONNECTION, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_CONNECTION, param_spec); + + param_spec = g_param_spec_string ("stun-server", "STUN server address", + "STUN server address", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_STUN_SERVER, param_spec); + + param_spec = g_param_spec_uint ("stun-port", "STUN port", + "STUN port.", + 0, G_MAXUINT16, + TPSIP_DEFAULT_STUN_PORT, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_STUN_PORT, param_spec); +} + +static void +rakia_media_manager_close_all (RakiaMediaManager *fac) +{ + RakiaMediaManagerPrivate *priv = TPSIP_MEDIA_MANAGER_GET_PRIVATE (fac); + + if (priv->status_changed_id != 0) + { + g_signal_handler_disconnect (priv->conn, + priv->status_changed_id); + priv->status_changed_id = 0; + } + + if (priv->channels != NULL) + { + GPtrArray *channels; + guint i; + + channels = priv->channels; + priv->channels = NULL; + + for (i = 0; i < channels->len; i++) + { + RakiaMediaChannel *chan = g_ptr_array_index (channels, i); + g_object_unref (chan); + } + + g_ptr_array_free (channels, TRUE); + } +} + +/** + * media_channel_closed_cb: + * Signal callback for when a media channel is closed. Removes the references + * that #RakiaMediaManager holds to them. + */ +static void +media_channel_closed_cb (RakiaMediaChannel *chan, gpointer user_data) +{ + RakiaMediaManager *fac = TPSIP_MEDIA_MANAGER (user_data); + RakiaMediaManagerPrivate *priv = TPSIP_MEDIA_MANAGER_GET_PRIVATE (fac); + + tp_channel_manager_emit_channel_closed_for_object (fac, + TP_EXPORTABLE_CHANNEL (chan)); + + if (priv->channels) + { + g_ptr_array_remove_fast (priv->channels, chan); + g_object_unref (chan); + } +} + +/** + * new_media_channel + * + * Creates a new empty RakiaMediaChannel. + */ +static RakiaMediaChannel * +new_media_channel (RakiaMediaManager *fac, + TpHandle initiator, + TpHandle maybe_peer, + RakiaMediaChannelCreationFlags flags) +{ + RakiaMediaManagerPrivate *priv; + RakiaMediaChannel *chan = NULL; + gchar *object_path; + const gchar *nat_traversal = "none"; + gboolean initial_audio; + gboolean initial_video; + gboolean immutable_streams = FALSE; + + g_assert (initiator != 0); + + priv = TPSIP_MEDIA_MANAGER_GET_PRIVATE (fac); + + object_path = g_strdup_printf ("%s/MediaChannel%u", priv->conn->object_path, + priv->channel_index++); + + DEBUG("channel object path %s", object_path); + + initial_audio = ((flags & TPSIP_MEDIA_CHANNEL_CREATE_WITH_AUDIO) != 0); + initial_video = ((flags & TPSIP_MEDIA_CHANNEL_CREATE_WITH_VIDEO) != 0); + + g_object_get (priv->conn, + "immutable-streams", &immutable_streams, + NULL); + + if (priv->stun_server != NULL) + { + nat_traversal = "stun"; + } + + chan = g_object_new (TPSIP_TYPE_MEDIA_CHANNEL, + "connection", priv->conn, + "object-path", object_path, + "handle", maybe_peer, + "initiator", initiator, + "initial-audio", initial_audio, + "initial-video", initial_video, + "immutable-streams", immutable_streams, + "nat-traversal", nat_traversal, + NULL); + + g_free (object_path); + + if (priv->stun_server != NULL) + { + g_object_set ((GObject *) chan, "stun-server", priv->stun_server, NULL); + if (priv->stun_port != 0) + g_object_set ((GObject *) chan, "stun-port", priv->stun_port, NULL); + } + + g_signal_connect (chan, "closed", G_CALLBACK (media_channel_closed_cb), fac); + + g_ptr_array_add (priv->channels, chan); + + return chan; +} + +static void +incoming_call_cb (RakiaMediaChannel *channel, + RakiaMediaManager *fac) +{ + g_signal_handlers_disconnect_by_func (channel, + G_CALLBACK (incoming_call_cb), fac); + tp_channel_manager_emit_new_channel (fac, + TP_EXPORTABLE_CHANNEL (channel), NULL); +} + +static gboolean +rakia_nua_i_invite_cb (TpBaseConnection *conn, + const RakiaNuaEvent *ev, + tagi_t tags[], + RakiaMediaManager *fac) +{ + RakiaMediaChannel *channel; + TpHandle handle; + guint channel_flags = 0; + + /* figure out a handle for the identity */ + + handle = rakia_handle_by_requestor (conn, ev->sip); + if (!handle) + { + MESSAGE ("incoming INVITE with invalid sender information"); + nua_respond (ev->nua_handle, 400, "Invalid From address", TAG_END()); + return TRUE; + } + + DEBUG("Got incoming invite from <%s>", + rakia_handle_inspect (conn, handle)); + + if (handle == conn->self_handle) + { + DEBUG("cannot handle calls from self"); + nua_respond (ev->nua_handle, 501, "Calls from self are not supported", TAG_END()); + return TRUE; + } + + channel = new_media_channel (fac, handle, handle, channel_flags); + + rakia_handle_unref (conn, handle); + + /* We delay emission of NewChannel(s) until we have the data on + * initial media */ + g_signal_connect (channel, "incoming-call", + G_CALLBACK (incoming_call_cb), fac); + + rakia_media_channel_attach_to_nua_handle (channel, ev->nua_handle); + + return TRUE; +} + +static void +connection_status_changed_cb (RakiaBaseConnection *conn, + guint status, + guint reason, + RakiaMediaManager *self) +{ + RakiaMediaManagerPrivate *priv = TPSIP_MEDIA_MANAGER_GET_PRIVATE (self); + + switch (status) + { + case TP_CONNECTION_STATUS_CONNECTED: + + priv->invite_received_id = g_signal_connect (conn, + "nua-event::nua_i_invite", + G_CALLBACK (rakia_nua_i_invite_cb), self); + + break; + case TP_CONNECTION_STATUS_DISCONNECTED: + + rakia_media_manager_close_all (self); + + if (priv->invite_received_id != 0) + { + g_signal_handler_disconnect (conn, priv->invite_received_id); + priv->invite_received_id = 0; + } + + break; + default: + break; + } +} + +static void +rakia_media_manager_constructed (GObject *object) +{ + RakiaMediaManager *self = TPSIP_MEDIA_MANAGER (object); + RakiaMediaManagerPrivate *priv = TPSIP_MEDIA_MANAGER_GET_PRIVATE (self); + GObjectClass *parent_object_class = + G_OBJECT_CLASS (rakia_media_manager_parent_class); + + if (parent_object_class->constructed != NULL) + parent_object_class->constructed (object); + + priv->status_changed_id = g_signal_connect (priv->conn, + "status-changed", (GCallback) connection_status_changed_cb, object); +} + +static void +rakia_media_manager_foreach_channel (TpChannelManager *manager, + TpExportableChannelFunc foreach, + gpointer user_data) +{ + RakiaMediaManager *fac = TPSIP_MEDIA_MANAGER (manager); + RakiaMediaManagerPrivate *priv = TPSIP_MEDIA_MANAGER_GET_PRIVATE (fac); + guint i; + + for (i = 0; i < priv->channels->len; i++) + { + TpExportableChannel *channel = TP_EXPORTABLE_CHANNEL ( + g_ptr_array_index (priv->channels, i)); + + foreach (channel, user_data); + } +} + +static const gchar * const media_channel_fixed_properties[] = { + TP_IFACE_CHANNEL ".ChannelType", + TP_IFACE_CHANNEL ".TargetHandleType", + NULL +}; + +static const gchar * const named_channel_allowed_properties[] = { + TP_IFACE_CHANNEL ".TargetHandle", + TP_IFACE_CHANNEL ".TargetID", + TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA ".InitialAudio", + TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA ".InitialVideo", + NULL +}; + +/* not advertised in type_foreach_channel_class - can only be requested with + * RequestChannel, not with CreateChannel/EnsureChannel */ +static const gchar * const anon_channel_allowed_properties[] = { + NULL +}; + +static void +rakia_media_manager_type_foreach_channel_class (GType type, + TpChannelManagerTypeChannelClassFunc func, + gpointer user_data) +{ + GHashTable *table = g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, (GDestroyNotify) tp_g_value_slice_free); + GValue *value, *handle_type_value; + + value = tp_g_value_slice_new (G_TYPE_STRING); + g_value_set_static_string (value, TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA); + g_hash_table_insert (table, TP_IFACE_CHANNEL ".ChannelType", value); + + handle_type_value = tp_g_value_slice_new (G_TYPE_UINT); + /* no uint value yet - we'll change it for each channel class */ + g_hash_table_insert (table, TP_IFACE_CHANNEL ".TargetHandleType", + handle_type_value); + + g_value_set_uint (handle_type_value, TP_HANDLE_TYPE_CONTACT); + func (type, table, named_channel_allowed_properties, user_data); + + g_hash_table_destroy (table); +} + +typedef enum +{ + METHOD_REQUEST, + METHOD_CREATE, + METHOD_ENSURE, +} RequestMethod; + +static gboolean +rakia_media_manager_requestotron (TpChannelManager *manager, + gpointer request_token, + GHashTable *request_properties, + RequestMethod method) +{ + RakiaMediaManager *self = TPSIP_MEDIA_MANAGER (manager); + RakiaMediaManagerPrivate *priv = TPSIP_MEDIA_MANAGER_GET_PRIVATE (self); + TpBaseConnection *conn = (TpBaseConnection *) priv->conn; + TpHandleType handle_type; + TpHandle handle; + RakiaMediaChannel *channel = NULL; + GError *error = NULL; + GSList *request_tokens; + guint chan_flags = 0; + gboolean require_target_handle; + gboolean add_peer_to_remote_pending; + + /* Supported modes of operation: + * - RequestChannel(None, 0): + * channel is anonymous; + * caller uses RequestStreams to set the peer and start the call. + * - RequestChannel(Contact, n) where n != 0: + * channel has TargetHandle=n; + * n is in remote pending; + * call is started when caller calls RequestStreams. + * - CreateChannel({THT: Contact, TH: n}): + * channel has TargetHandle=n + * n is not in the group interface at all + * call is started when caller calls RequestStreams. + * - EnsureChannel({THT: Contact, TH: n}): + * look for a channel whose peer is n, and return that if found with + * whatever properties and group membership it has; + * otherwise the same as into CreateChannel + */ + switch (method) + { + case METHOD_REQUEST: + require_target_handle = FALSE; + add_peer_to_remote_pending = TRUE; + break; + case METHOD_CREATE: + case METHOD_ENSURE: + require_target_handle = TRUE; + add_peer_to_remote_pending = FALSE; + break; + default: + g_assert_not_reached (); + } + + if (tp_strdiff (tp_asv_get_string (request_properties, + TP_IFACE_CHANNEL ".ChannelType"), + TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA)) + return FALSE; + + handle_type = tp_asv_get_uint32 (request_properties, + TP_IFACE_CHANNEL ".TargetHandleType", NULL); + + handle = tp_asv_get_uint32 (request_properties, + TP_IFACE_CHANNEL ".TargetHandle", NULL); + + switch (handle_type) + { + case TP_HANDLE_TYPE_NONE: + g_assert (handle == 0); + + if (require_target_handle) + { + g_set_error (&error, TP_ERRORS, TP_ERROR_NOT_IMPLEMENTED, + "A valid Contact handle must be provided when requesting a media " + "channel"); + goto error; + } + + if (tp_channel_manager_asv_has_unknown_properties (request_properties, + media_channel_fixed_properties, anon_channel_allowed_properties, + &error)) + goto error; + + channel = new_media_channel (self, conn->self_handle, 0, 0); + break; + + case TP_HANDLE_TYPE_CONTACT: + g_assert (handle != 0); + + if (tp_channel_manager_asv_has_unknown_properties (request_properties, + media_channel_fixed_properties, named_channel_allowed_properties, + &error)) + goto error; + + /* Calls to self are problematic in terms of StreamedMedia channel + * interface and its semantically required Group member changes; + * we disable them until a better API is available through + * Call channel type */ + if (handle == conn->self_handle) + { + g_set_error (&error, TP_ERRORS, TP_ERROR_NOT_IMPLEMENTED, + "Cannot call self"); + goto error; + } + + if (method == METHOD_ENSURE) + { + guint i; + TpHandle peer = 0; + + for (i = 0; i < priv->channels->len; i++) + { + channel = g_ptr_array_index (priv->channels, i); + g_object_get (channel, "peer", &peer, NULL); + + if (peer == handle) + { + tp_channel_manager_emit_request_already_satisfied (self, + request_token, TP_EXPORTABLE_CHANNEL (channel)); + return TRUE; + } + } + } + + if (tp_asv_get_boolean (request_properties, + TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA ".InitialAudio", NULL)) + chan_flags |= TPSIP_MEDIA_CHANNEL_CREATE_WITH_AUDIO; + + if (tp_asv_get_boolean (request_properties, + TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA ".InitialVideo", NULL)) + chan_flags |= TPSIP_MEDIA_CHANNEL_CREATE_WITH_VIDEO; + + channel = new_media_channel (self, conn->self_handle, handle, chan_flags); + + if (add_peer_to_remote_pending) + { + if (!_rakia_media_channel_add_member ((GObject *) channel, handle, + "", &error)) + { + /* FIXME: do we really want to emit Closed in this case? + * There wasn't a NewChannel/NewChannels emission */ + rakia_media_channel_close (channel); + goto error; + } + } + + break; + + default: + return FALSE; + } + + g_assert (channel != NULL); + + request_tokens = g_slist_prepend (NULL, request_token); + tp_channel_manager_emit_new_channel (self, + TP_EXPORTABLE_CHANNEL (channel), request_tokens); + g_slist_free (request_tokens); + + rakia_media_channel_create_initial_streams (channel); + + return TRUE; + +error: + tp_channel_manager_emit_request_failed (self, request_token, + error->domain, error->code, error->message); + g_error_free (error); + return TRUE; +} + +static gboolean +rakia_media_manager_request_channel (TpChannelManager *manager, + gpointer request_token, + GHashTable *request_properties) +{ + return rakia_media_manager_requestotron (manager, request_token, + request_properties, METHOD_REQUEST); +} + +static gboolean +rakia_media_manager_create_channel (TpChannelManager *manager, + gpointer request_token, + GHashTable *request_properties) +{ + return rakia_media_manager_requestotron (manager, request_token, + request_properties, METHOD_CREATE); +} + +static gboolean +rakia_media_manager_ensure_channel (TpChannelManager *manager, + gpointer request_token, + GHashTable *request_properties) +{ + return rakia_media_manager_requestotron (manager, request_token, + request_properties, METHOD_ENSURE); +} + +static void +channel_manager_iface_init (gpointer g_iface, + gpointer iface_data) +{ + TpChannelManagerIface *iface = g_iface; + + iface->foreach_channel = rakia_media_manager_foreach_channel; + iface->type_foreach_channel_class = + rakia_media_manager_type_foreach_channel_class; + iface->request_channel = rakia_media_manager_request_channel; + iface->create_channel = rakia_media_manager_create_channel; + iface->ensure_channel = rakia_media_manager_ensure_channel; +} diff --git a/rakia/media-manager.h b/rakia/media-manager.h new file mode 100644 index 0000000..7b962be --- /dev/null +++ b/rakia/media-manager.h @@ -0,0 +1,57 @@ +/* + * rakia/media-manager.h - Media channel manager for SIP + * Copyright (C) 2007 Collabora Ltd. + * Copyright (C) 2007-2008 Nokia Corporation + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __TPSIP_MEDIA_MANAGER_H__ +#define __TPSIP_MEDIA_MANAGER_H__ + +#include <glib-object.h> + +G_BEGIN_DECLS + +typedef struct _RakiaMediaManager RakiaMediaManager; +typedef struct _RakiaMediaManagerClass RakiaMediaManagerClass; + +struct _RakiaMediaManagerClass { + GObjectClass parent_class; +}; + +struct _RakiaMediaManager { + GObject parent; +}; + +GType rakia_media_manager_get_type(void); + +/* TYPE MACROS */ +#define TPSIP_TYPE_MEDIA_MANAGER \ + (rakia_media_manager_get_type()) +#define TPSIP_MEDIA_MANAGER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), TPSIP_TYPE_MEDIA_MANAGER, RakiaMediaManager)) +#define TPSIP_MEDIA_MANAGER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), TPSIP_TYPE_MEDIA_MANAGER, RakiaMediaManagerClass)) +#define TPSIP_IS_MEDIA_MANAGER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), TPSIP_TYPE_MEDIA_MANAGER)) +#define TPSIP_IS_MEDIA_MANAGER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), TPSIP_TYPE_MEDIA_MANAGER)) +#define TPSIP_MEDIA_MANAGER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), TPSIP_TYPE_MEDIA_MANAGER, RakiaMediaManagerClass)) + +G_END_DECLS + +#endif diff --git a/rakia/media-session.c b/rakia/media-session.c new file mode 100644 index 0000000..2766321 --- /dev/null +++ b/rakia/media-session.c @@ -0,0 +1,2243 @@ +/* + * sip-media-session.c - Source for RakiaMediaSession + * Copyright (C) 2005 Collabora Ltd. + * Copyright (C) 2005-2010 Nokia Corporation + * @author Kai Vehmanen <first.surname@nokia.com> + * @author Mikhail Zabaluev <mikhail.zabaluev@nokia.com> + * + * Based on telepathy-gabble implementation (gabble-media-session). + * @author Ole Andre Vadla Ravnaas <ole.andre.ravnaas@collabora.co.uk> + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include "rakia/media-session.h" + +#include <dbus/dbus-glib.h> +#include <stdlib.h> +#include <time.h> +#include <string.h> + +#include <sofia-sip/sip_status.h> + +#include <telepathy-glib/dbus.h> +#include <telepathy-glib/errors.h> +#include <telepathy-glib/gtypes.h> +#include <telepathy-glib/interfaces.h> +#include <telepathy-glib/svc-media-interfaces.h> + +#include "config.h" + +#include <rakia/base-connection.h> + +#include "rakia/media-channel.h" +#include "rakia/media-stream.h" + +#include "signals-marshal.h" + +#define DEBUG_FLAG TPSIP_DEBUG_MEDIA +#include "rakia/debug.h" + +/* The timeout for outstanding re-INVITE transactions in seconds. + * Chosen to match the allowed cancellation timeout for proxies + * described in RFC 3261 Section 13.3.1.1 */ +#define TPSIP_REINVITE_TIMEOUT 180 + +static void session_handler_iface_init (gpointer, gpointer); + +G_DEFINE_TYPE_WITH_CODE(RakiaMediaSession, + rakia_media_session, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_MEDIA_SESSION_HANDLER, + session_handler_iface_init) + ) + +/* signal enum */ +enum +{ + SIG_STATE_CHANGED, + SIG_LAST_SIGNAL +}; + +/* properties */ +enum +{ + PROP_MEDIA_CHANNEL = 1, + PROP_DBUS_DAEMON, + PROP_OBJECT_PATH, + PROP_NUA_OP, + PROP_PEER, + PROP_HOLD_STATE, + PROP_HOLD_STATE_REASON, + PROP_REMOTE_PTIME, + PROP_REMOTE_MAX_PTIME, + PROP_RTCP_ENABLED, + PROP_LOCAL_IP_ADDRESS, + PROP_STUN_SERVERS, + LAST_PROPERTY +}; + +static guint signals[SIG_LAST_SIGNAL] = {0}; + +#ifdef ENABLE_DEBUG + +/** + * Media session states: + * - created, objects created, local cand/codec query ongoing + * - invite-sent, an INVITE with local SDP sent, awaiting response + * - invite-received, a remote INVITE received, response is pending + * - response-received, a 200 OK received, codec intersection is in progress + * - active, codecs and candidate pairs have been negotiated (note, + * SteamEngine might still fail to verify connectivity and report + * an error) + * - reinvite-sent, a local re-INVITE sent, response is pending + * - reinvite-received, a remote re-INVITE received, response is pending + * - ended, session has ended + */ +static const char* session_states[] = +{ + "created", + "invite-sent", + "invite-received", + "response-received", + "active", + "reinvite-sent", + "reinvite-received", + "reinvite-pending", + "ended" +}; + +#endif /* ENABLE_DEBUG */ + +/* private structure */ +typedef struct _RakiaMediaSessionPrivate RakiaMediaSessionPrivate; + +struct _RakiaMediaSessionPrivate +{ + TpDBusDaemon *dbus_daemon; + RakiaMediaChannel *channel; /* see gobj. prop. 'media-channel' */ + gchar *object_path; /* see gobj. prop. 'object-path' */ + nua_handle_t *nua_op; /* see gobj. prop. 'nua-handle' */ + TpHandle peer; /* see gobj. prop. 'peer' */ + gchar *local_ip_address; /* see gobj. prop. 'local-ip-address' */ + gchar *remote_ptime; /* see gobj. prop. 'remote-ptime' */ + gchar *remote_max_ptime; /* see gobj. prop. 'remote-max-ptime' */ + gboolean rtcp_enabled; /* see gobj. prop. 'rtcp-enabled' */ + RakiaMediaSessionState state; /* session state */ + TpLocalHoldState hold_state; /* local hold state aggregated from stream directions */ + TpLocalHoldStateReason hold_reason; /* last used hold state change reason */ + nua_saved_event_t saved_event[1]; /* Saved incoming request event */ + gint local_non_ready; /* number of streams with local information update pending */ + guint remote_stream_count; /* number of streams last seen in a remote offer */ + guint glare_timer_id; + su_home_t *home; /* Sofia memory home for remote SDP session structure */ + su_home_t *backup_home; /* Sofia memory home for previous generation remote SDP session*/ + sdp_session_t *remote_sdp; /* last received remote session */ + sdp_session_t *backup_remote_sdp; /* previous remote session */ + GPtrArray *streams; + gboolean remote_initiated; /*< session is remotely intiated */ + gboolean accepted; /*< session has been locally accepted for use */ + gboolean se_ready; /*< connection established with stream-engine */ + gboolean pending_offer; /*< local media have been changed, but a re-INVITE is pending */ + gboolean dispose_has_run; +}; + +#define TPSIP_MEDIA_SESSION_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), TPSIP_TYPE_MEDIA_SESSION, RakiaMediaSessionPrivate)) + +static void rakia_media_session_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec); +static void rakia_media_session_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec); + +static RakiaMediaStream * +rakia_media_session_get_stream (RakiaMediaSession *self, + guint stream_id, + GError **error); + +static void priv_request_response_step (RakiaMediaSession *session); +static void priv_session_invite (RakiaMediaSession *session, gboolean reinvite); +static void priv_local_media_changed (RakiaMediaSession *session); +static gboolean priv_update_remote_media (RakiaMediaSession *session, + gboolean authoritative); +static void priv_save_event (RakiaMediaSession *self); +static void priv_zap_event (RakiaMediaSession *self); + +static void rakia_media_session_init (RakiaMediaSession *obj) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (obj); + + priv->state = TPSIP_MEDIA_SESSION_STATE_CREATED; + priv->hold_state = TP_LOCAL_HOLD_STATE_UNHELD; + priv->hold_reason = TP_LOCAL_HOLD_STATE_REASON_NONE; + priv->rtcp_enabled = TRUE; + + /* allocate any data required by the object here */ + priv->streams = g_ptr_array_new (); +} + +static GObject * +rakia_media_session_constructor (GType type, guint n_props, + GObjectConstructParam *props) +{ + GObject *obj; + RakiaMediaSessionPrivate *priv; + + obj = G_OBJECT_CLASS (rakia_media_session_parent_class)-> + constructor (type, n_props, props); + priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (TPSIP_MEDIA_SESSION (obj)); + + g_assert (TP_IS_DBUS_DAEMON (priv->dbus_daemon)); + tp_dbus_daemon_register_object (priv->dbus_daemon, priv->object_path, obj); + + return obj; +} + +static void rakia_media_session_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + RakiaMediaSession *session = TPSIP_MEDIA_SESSION (object); + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + + switch (property_id) + { + case PROP_DBUS_DAEMON: + g_value_set_object (value, priv->dbus_daemon); + break; + case PROP_MEDIA_CHANNEL: + g_value_set_object (value, priv->channel); + break; + case PROP_OBJECT_PATH: + g_value_set_string (value, priv->object_path); + break; + case PROP_NUA_OP: + g_value_set_pointer (value, priv->nua_op); + break; + case PROP_PEER: + g_value_set_uint (value, priv->peer); + break; + case PROP_HOLD_STATE: + g_value_set_uint (value, priv->hold_state); + break; + case PROP_HOLD_STATE_REASON: + g_value_set_uint (value, priv->hold_reason); + break; + case PROP_REMOTE_PTIME: + g_value_set_string (value, priv->remote_ptime); + break; + case PROP_REMOTE_MAX_PTIME: + g_value_set_string (value, priv->remote_max_ptime); + break; + case PROP_LOCAL_IP_ADDRESS: + g_value_set_string (value, priv->local_ip_address); + break; + case PROP_RTCP_ENABLED: + g_value_set_boolean (value, priv->rtcp_enabled); + break; + case PROP_STUN_SERVERS: + { + /* TODO: should be able to get all entries from the DNS lookup(s). + * At the moment, rawudp ignores all servers except the first one. */ + GPtrArray *servers; + gchar *stun_server = NULL; + guint stun_port = TPSIP_DEFAULT_STUN_PORT; + + g_return_if_fail (priv->channel != NULL); + + g_object_get (priv->channel, + "stun-server", &stun_server, + "stun-port", &stun_port, + NULL); + + servers = g_ptr_array_new (); + + if (stun_server != NULL) + { + GValue addr = { 0 }; + const GType addr_type = TP_STRUCT_TYPE_SOCKET_ADDRESS_IP; + + g_value_init (&addr, addr_type); + g_value_take_boxed (&addr, + dbus_g_type_specialized_construct (addr_type)); + + dbus_g_type_struct_set (&addr, + 0, stun_server, + 1, (guint16) stun_port, + G_MAXUINT); + + g_ptr_array_add (servers, g_value_get_boxed (&addr)); + + g_free (stun_server); + } + + g_value_take_boxed (value, servers); + } + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void rakia_media_session_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + RakiaMediaSession *session = TPSIP_MEDIA_SESSION (object); + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + + switch (property_id) + { + case PROP_DBUS_DAEMON: + g_assert (priv->dbus_daemon == NULL); /* construct-only */ + priv->dbus_daemon = g_value_dup_object (value); + break; + case PROP_MEDIA_CHANNEL: + priv->channel = TPSIP_MEDIA_CHANNEL (g_value_get_object (value)); + break; + case PROP_OBJECT_PATH: + g_assert (priv->object_path == NULL); + priv->object_path = g_value_dup_string (value); + break; + case PROP_NUA_OP: + /* you can only set the NUA handle once - migrating a media session + * between two NUA handles makes no sense */ + g_return_if_fail (priv->nua_op == NULL); + priv->nua_op = g_value_get_pointer (value); + nua_handle_ref (priv->nua_op); + break; + case PROP_PEER: + priv->peer = g_value_get_uint (value); + break; + case PROP_LOCAL_IP_ADDRESS: + g_assert (priv->local_ip_address == NULL); + priv->local_ip_address = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void rakia_media_session_dispose (GObject *object); +static void rakia_media_session_finalize (GObject *object); + +static void +rakia_media_session_class_init (RakiaMediaSessionClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GParamSpec *param_spec; + + g_type_class_add_private (klass, sizeof (RakiaMediaSessionPrivate)); + + object_class->constructor = rakia_media_session_constructor; + + object_class->get_property = rakia_media_session_get_property; + object_class->set_property = rakia_media_session_set_property; + + object_class->dispose = rakia_media_session_dispose; + object_class->finalize = rakia_media_session_finalize; + + param_spec = g_param_spec_object ("dbus-daemon", "TpDBusDaemon", + "Connection to D-Bus.", TP_TYPE_DBUS_DAEMON, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_DBUS_DAEMON, param_spec); + + param_spec = g_param_spec_object ("media-channel", "RakiaMediaChannel object", + "SIP media channel object that owns this media session object" + " (not reference counted).", + TPSIP_TYPE_MEDIA_CHANNEL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_MEDIA_CHANNEL, param_spec); + + param_spec = g_param_spec_string ("object-path", "D-Bus object path", + "The D-Bus object path used for this object on the bus.", + NULL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_OBJECT_PATH, param_spec); + + param_spec = g_param_spec_pointer ("nua-handle", "NUA handle", + "NUA stack operation handle associated with this media session.", + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_NUA_OP, param_spec); + + param_spec = g_param_spec_uint ("peer", "Session peer", + "The TpHandle representing the contact with whom this session communicates.", + 0, G_MAXUINT32, + 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_PEER, param_spec); + + param_spec = g_param_spec_uint ("hold-state", "Local hold state", + "The current Local_Hold_State value as reported by the Hold interface", + TP_LOCAL_HOLD_STATE_UNHELD, TP_LOCAL_HOLD_STATE_PENDING_UNHOLD, + TP_LOCAL_HOLD_STATE_UNHELD, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_HOLD_STATE, param_spec); + + param_spec = g_param_spec_uint ("hold-state-reason", + "Local hold state change reason", + "The last Local_Hold_State_Reason value as reported by the Hold interface", + TP_LOCAL_HOLD_STATE_REASON_NONE, + TP_LOCAL_HOLD_STATE_REASON_RESOURCE_NOT_AVAILABLE, + TP_LOCAL_HOLD_STATE_REASON_NONE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_HOLD_STATE_REASON, param_spec); + + param_spec = g_param_spec_string ("remote-ptime", + "a=ptime value of remote media session", + "Value of the a=ptime attribute of the remote media session, or NULL", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_REMOTE_PTIME, param_spec); + + param_spec = g_param_spec_string ("remote-max-ptime", + "a=maxptime value of remote media session", + "Value of the a=maxptime attribute of the remote media session, or NULL", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_REMOTE_MAX_PTIME, param_spec); + + param_spec = g_param_spec_string ("local-ip-address", "Local IP address", + "The local IP address preferred for media streams", + NULL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_LOCAL_IP_ADDRESS, param_spec); + + param_spec = g_param_spec_boolean ("rtcp-enabled", "RTCP enabled", + "Is RTCP enabled session-wide", + TRUE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_RTCP_ENABLED, param_spec); + + param_spec = g_param_spec_boxed ("stun-servers", "STUN servers", + "Array of IP address-port pairs for available STUN servers", + TP_ARRAY_TYPE_SOCKET_ADDRESS_IP_LIST, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_STUN_SERVERS, param_spec); + + signals[SIG_STATE_CHANGED] = + g_signal_new ("state-changed", + G_OBJECT_CLASS_TYPE (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + 0, + NULL, NULL, + _rakia_marshal_VOID__UINT_UINT, + G_TYPE_NONE, 2, G_TYPE_UINT, G_TYPE_UINT); +} + +static void +rakia_media_session_dispose (GObject *object) +{ + RakiaMediaSession *self = TPSIP_MEDIA_SESSION (object); + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (self); + + if (priv->dispose_has_run) + return; + + DEBUG("enter"); + + priv->dispose_has_run = TRUE; + + if (priv->glare_timer_id) + g_source_remove (priv->glare_timer_id); + + tp_clear_object (&priv->dbus_daemon); + + if (G_OBJECT_CLASS (rakia_media_session_parent_class)->dispose) + G_OBJECT_CLASS (rakia_media_session_parent_class)->dispose (object); + + DEBUG("exit"); +} + +static void +rakia_media_session_finalize (GObject *object) +{ + RakiaMediaSession *self = TPSIP_MEDIA_SESSION (object); + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (self); + guint i; + + /* terminating the session should have discarded the NUA handle */ + g_assert (priv->nua_op == NULL); + + /* free any data held directly by the object here */ + + for (i = 0; i < priv->streams->len; i++) { + RakiaMediaStream *stream = g_ptr_array_index (priv->streams, i); + if (stream != NULL) + { + WARNING ("stream %u (%p) left over, reaping", i, stream); + g_object_unref (stream); + } + } + g_ptr_array_free(priv->streams, TRUE); + + priv_zap_event (self); + + if (priv->home != NULL) + su_home_unref (priv->home); + if (priv->backup_home != NULL) + su_home_unref (priv->backup_home); + + g_free (priv->remote_ptime); + g_free (priv->remote_max_ptime); + g_free (priv->local_ip_address); + g_free (priv->object_path); + + G_OBJECT_CLASS (rakia_media_session_parent_class)->finalize (object); + + DEBUG("exit"); +} + + + +/** + * rakia_media_session_error + * + * Implements DBus method Error + * on interface org.freedesktop.Telepathy.Media.SessionHandler + */ +static void +rakia_media_session_error (TpSvcMediaSessionHandler *iface, + guint errno, + const gchar *message, + DBusGMethodInvocation *context) +{ + RakiaMediaSession *obj = TPSIP_MEDIA_SESSION (iface); + + SESSION_DEBUG (obj, "Media.SessionHandler::Error called (%s), terminating session", message); + + rakia_media_session_terminate (obj); + + tp_svc_media_session_handler_return_from_error (context); +} + +static void priv_emit_new_stream (RakiaMediaSession *self, + RakiaMediaStream *stream) +{ + gchar *object_path; + guint id; + guint media_type; + guint direction; + + g_object_get (stream, + "object-path", &object_path, + "id", &id, + "media-type", &media_type, + "direction", &direction, + NULL); + + /* note: all of the streams are bidirectional from farsight's point of view, it's + * just in the signalling they change */ + + tp_svc_media_session_handler_emit_new_stream_handler ( + (TpSvcMediaSessionHandler *)self, object_path, id, media_type, + direction); + + g_free (object_path); +} + + +/** + * rakia_media_session_ready + * + * Implements DBus method Ready + * on interface org.freedesktop.Telepathy.Media.SessionHandler + */ +static void +rakia_media_session_ready (TpSvcMediaSessionHandler *iface, + DBusGMethodInvocation *context) +{ + RakiaMediaSession *obj = TPSIP_MEDIA_SESSION (iface); + RakiaMediaSessionPrivate *priv; + guint i; + + DEBUG ("enter"); + + priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (obj); + + if (!priv->se_ready) + { + priv->se_ready = TRUE; + + /* note: streams are generated in priv_create_media_stream() */ + + for (i = 0; i < priv->streams->len; i++) + { + RakiaMediaStream *stream = g_ptr_array_index (priv->streams, i); + if (stream) + priv_emit_new_stream (obj, stream); + } + } + + tp_svc_media_session_handler_return_from_ready (context); +} + +/*********************************************************************** + * Helper functions follow (not based on generated templates) + ***********************************************************************/ + +TpHandle +rakia_media_session_get_peer (RakiaMediaSession *session) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + return priv->peer; +} + +RakiaMediaSessionState +rakia_media_session_get_state (RakiaMediaSession *session) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + return priv->state; +} + +static gboolean +rakia_media_session_supports_media_type (guint media_type) +{ + switch (media_type) + { + case TP_MEDIA_STREAM_TYPE_AUDIO: + case TP_MEDIA_STREAM_TYPE_VIDEO: + return TRUE; + } + return FALSE; +} + +static void +priv_close_all_streams (RakiaMediaSession *session) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + guint i; + for (i = 0; i < priv->streams->len; i++) + { + RakiaMediaStream *stream; + stream = g_ptr_array_index (priv->streams, i); + if (stream != NULL) + rakia_media_stream_close (stream); + g_assert (g_ptr_array_index (priv->streams, i) == NULL); + } +} + +static void +priv_apply_streams_pending_direction (RakiaMediaSession *session, + guint pending_send_mask) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + RakiaMediaStream *stream; + guint i; + + /* If there has been a local change pending a re-INVITE, + * suspend remote approval until the next transaction */ + if (priv->pending_offer) + pending_send_mask &= ~(guint)TP_MEDIA_STREAM_PENDING_REMOTE_SEND; + + /* Apply the pending direction changes */ + for (i = 0; i < priv->streams->len; i++) + { + stream = g_ptr_array_index(priv->streams, i); + if (stream != NULL) + rakia_media_stream_apply_pending_direction (stream, pending_send_mask); + } +} + +void +rakia_media_session_change_state (RakiaMediaSession *session, + RakiaMediaSessionState new_state) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + guint old_state; + + if (priv->state == new_state) + return; + + old_state = priv->state; + priv->state = new_state; + + SESSION_DEBUG (session, "state change: %s -> %s", + session_states[old_state], + session_states[new_state]); + + switch (new_state) + { + case TPSIP_MEDIA_SESSION_STATE_CREATED: + case TPSIP_MEDIA_SESSION_STATE_INVITE_RECEIVED: + case TPSIP_MEDIA_SESSION_STATE_REINVITE_RECEIVED: + case TPSIP_MEDIA_SESSION_STATE_INVITE_SENT: + case TPSIP_MEDIA_SESSION_STATE_REINVITE_SENT: + case TPSIP_MEDIA_SESSION_STATE_RESPONSE_RECEIVED: + case TPSIP_MEDIA_SESSION_STATE_REINVITE_PENDING: + break; + case TPSIP_MEDIA_SESSION_STATE_ACTIVE: + /* Apply any pending remote send after outgoing INVITEs. + * We don't want automatic removal of pending local send after + * responding to incoming re-INVITEs, however */ + priv_apply_streams_pending_direction (session, + TP_MEDIA_STREAM_PENDING_REMOTE_SEND); + break; + case TPSIP_MEDIA_SESSION_STATE_ENDED: + priv_close_all_streams (session); + DEBUG("destroying the NUA handle %p", priv->nua_op); + if (priv->nua_op != NULL) + { + nua_handle_destroy (priv->nua_op); + priv->nua_op = NULL; + } + break; + + /* Don't add default because we want to be warned by the compiler + * about unhandled states */ + } + + g_signal_emit (session, signals[SIG_STATE_CHANGED], 0, old_state, new_state); + + if (new_state == TPSIP_MEDIA_SESSION_STATE_ACTIVE && priv->pending_offer) + priv_session_invite (session, TRUE); +} + +#ifdef ENABLE_DEBUG +void +rakia_media_session_debug (RakiaMediaSession *session, + const gchar *format, ...) +{ + RakiaMediaSessionPrivate *priv; + va_list list; + gchar buf[240]; + + if (!rakia_debug_flag_is_set (DEBUG_FLAG)) + return; + + priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + + va_start (list, format); + + g_vsnprintf (buf, sizeof (buf), format, list); + + va_end (list); + + DEBUG ("SIP media session [%-17s]: %s", + session_states[priv->state], buf); +} +#endif /* ENABLE_DEBUG */ + +void rakia_media_session_terminate (RakiaMediaSession *session) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + + DEBUG ("enter"); + + if (priv->state == TPSIP_MEDIA_SESSION_STATE_ENDED) + return; + + /* XXX: taken care of by the state change? */ + priv_close_all_streams (session); + + if (priv->nua_op != NULL) + { + /* XXX: should the stack do pretty much the same + * (except freeing the saved event) upon nua_handle_destroy()? */ + switch (priv->state) + { + case TPSIP_MEDIA_SESSION_STATE_ACTIVE: + case TPSIP_MEDIA_SESSION_STATE_RESPONSE_RECEIVED: + case TPSIP_MEDIA_SESSION_STATE_REINVITE_SENT: + case TPSIP_MEDIA_SESSION_STATE_REINVITE_PENDING: + DEBUG("sending BYE"); + nua_bye (priv->nua_op, TAG_END()); + break; + case TPSIP_MEDIA_SESSION_STATE_INVITE_SENT: + DEBUG("sending CANCEL"); + nua_cancel (priv->nua_op, TAG_END()); + break; + case TPSIP_MEDIA_SESSION_STATE_INVITE_RECEIVED: + DEBUG("sending the 480 response to an incoming INVITE"); + nua_respond (priv->nua_op, 480, "Terminated", TAG_END()); + break; + case TPSIP_MEDIA_SESSION_STATE_REINVITE_RECEIVED: + if (priv->saved_event[0]) + { + DEBUG("sending the 480 response to an incoming re-INVITE"); + nua_respond (priv->nua_op, 480, "Terminated", + NUTAG_WITH(nua_saved_event_request (priv->saved_event)), + TAG_END()); + nua_destroy_event (priv->saved_event); + } + DEBUG("sending BYE to terminate the call itself"); + nua_bye (priv->nua_op, TAG_END()); + break; + default: + /* let the Sofia stack decide what do to */; + } + } + + rakia_media_session_change_state (session, TPSIP_MEDIA_SESSION_STATE_ENDED); +} + +gboolean +rakia_media_session_set_remote_media (RakiaMediaSession *session, + const sdp_session_t* sdp) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + gboolean authoritative; + + DEBUG ("enter"); + + if (priv->state == TPSIP_MEDIA_SESSION_STATE_INVITE_SENT + || priv->state == TPSIP_MEDIA_SESSION_STATE_REINVITE_SENT) + { + rakia_media_session_change_state ( + session, + TPSIP_MEDIA_SESSION_STATE_RESPONSE_RECEIVED); + } + else + { + /* Remember the m= line count in the remote offer, + * to match it with exactly this number of answer lines */ + sdp_media_t *media; + guint count = 0; + + for (media = sdp->sdp_media; media != NULL; media = media->m_next) + ++count; + + priv->remote_stream_count = count; + } + + /* Shortcut session non-updates */ + if (!sdp_session_cmp (priv->remote_sdp, sdp)) + goto finally; + + /* Delete a backup session structure, if any */ + if (priv->backup_remote_sdp != NULL) + { + priv->backup_remote_sdp = NULL; + g_assert (priv->backup_home != NULL); + su_home_unref (priv->backup_home); + priv->backup_home = NULL; + } + /* Back up the old session. + * The streams still need the old media descriptions */ + if (priv->remote_sdp != NULL) + { + g_assert (priv->home != NULL); + g_assert (priv->backup_home == NULL); + priv->backup_home = priv->home; + priv->backup_remote_sdp = priv->remote_sdp; + } + + /* Store the session description structure */ + priv->home = su_home_create (); + priv->remote_sdp = sdp_session_dup (priv->home, sdp); + g_return_val_if_fail (priv->remote_sdp != NULL, FALSE); + + authoritative = (priv->state == TPSIP_MEDIA_SESSION_STATE_INVITE_RECEIVED + || priv->state == TPSIP_MEDIA_SESSION_STATE_REINVITE_RECEIVED); + if (!priv_update_remote_media (session, authoritative)) + return FALSE; + +finally: + /* Make sure to always transition states and send out the response, + * even if no stream-engine roundtrips were initiated */ + priv_request_response_step (session); + return TRUE; +} + +void +priv_add_stream_list_entry (GPtrArray *list, + RakiaMediaStream *stream, + RakiaMediaSession *session) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + GValue entry = { 0 }; + GType stream_type; + guint id; + TpMediaStreamType type = TP_MEDIA_STREAM_TYPE_AUDIO; + TpMediaStreamState connection_state = TP_MEDIA_STREAM_STATE_CONNECTED; + TpMediaStreamDirection direction = TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL; + guint pending_send_flags = 0; + + g_assert(stream != NULL); + + g_object_get (stream, + "id", &id, + "media-type", &type, + "state", &connection_state, + "direction", &direction, + "pending-send-flags", &pending_send_flags, + NULL); + + stream_type = TP_STRUCT_TYPE_MEDIA_STREAM_INFO; + + g_value_init (&entry, stream_type); + g_value_take_boxed (&entry, + dbus_g_type_specialized_construct (stream_type)); + + dbus_g_type_struct_set (&entry, + 0, id, + 1, priv->peer, + 2, type, + 3, connection_state, + 4, direction, + 5, pending_send_flags, + G_MAXUINT); + + g_ptr_array_add (list, g_value_get_boxed (&entry)); +} + +gboolean rakia_media_session_request_streams (RakiaMediaSession *session, + const GArray *media_types, + GPtrArray *ret, + GError **error) +{ + guint i; + + DEBUG ("enter"); + + /* Validate the media types before creating any streams */ + for (i = 0; i < media_types->len; i++) { + guint media_type = g_array_index (media_types, guint, i); + if (!rakia_media_session_supports_media_type (media_type)) + { + g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "media type #%u is not supported", i); + return FALSE; + } + } + + for (i = 0; i < media_types->len; i++) { + guint media_type = g_array_index (media_types, guint, i); + RakiaMediaStream *stream; + + stream = rakia_media_session_add_stream (session, + media_type, + TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL, + TRUE); + + if (stream == NULL) + { + g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "creation of stream %u failed", i); + /* XXX: should we close the streams already created as part of + * this request, despite having emitted signals about them? */ + return FALSE; + } + + priv_add_stream_list_entry (ret, stream, session); + } + + priv_local_media_changed (session); + + return TRUE; +} + +gboolean +rakia_media_session_remove_streams (RakiaMediaSession *self, + const GArray *stream_ids, + GError **error) +{ + RakiaMediaStream *stream; + guint stream_id; + guint i; + + DEBUG ("enter"); + + for (i = 0; i < stream_ids->len; i++) + { + stream_id = g_array_index (stream_ids, guint, i); + stream = rakia_media_session_get_stream (self, stream_id, error); + if (stream == NULL) + return FALSE; + rakia_media_stream_close (stream); + } + + priv_local_media_changed (self); + + return TRUE; +} + +void rakia_media_session_list_streams (RakiaMediaSession *session, + GPtrArray *ret) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + RakiaMediaStream *stream; + guint i; + + for (i = 0; i < priv->streams->len; i++) + { + stream = g_ptr_array_index(priv->streams, i); + if (stream) + priv_add_stream_list_entry (ret, stream, session); + } +} + +gboolean +rakia_media_session_request_stream_direction (RakiaMediaSession *self, + guint stream_id, + guint direction, + GError **error) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (self); + RakiaMediaStream *stream; + + stream = rakia_media_session_get_stream (self, stream_id, error); + if (stream == NULL) + { + g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "stream %u does not exist", stream_id); + return FALSE; + } + + SESSION_DEBUG (self, "direction %u requested for stream %u", + direction, stream_id); + + if (priv->state == TPSIP_MEDIA_SESSION_STATE_INVITE_RECEIVED + || priv->state == TPSIP_MEDIA_SESSION_STATE_REINVITE_RECEIVED) + { + /* While processing a session offer, we can only mask out direction + * requested by the remote peer */ + direction &= rakia_media_stream_get_requested_direction (stream); + } + + rakia_media_stream_set_direction (stream, + direction, + TP_MEDIA_STREAM_PENDING_REMOTE_SEND); + + return TRUE; +} + +static void +priv_save_event (RakiaMediaSession *self) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (self); + RakiaBaseConnection *conn = NULL; + + priv_zap_event (self); + + g_object_get (priv->channel, "connection", &conn, NULL); + + g_return_if_fail (conn != NULL); + + rakia_base_connection_save_event (conn, priv->saved_event); + + g_object_unref (conn); + +#ifdef ENABLE_DEBUG + { + nua_event_data_t const *ev_data = nua_event_data (priv->saved_event); + g_assert (ev_data != NULL); + DEBUG("saved the last event: %s %hd %s", nua_event_name (ev_data->e_event), ev_data->e_status, ev_data->e_phrase); + } +#endif +} + +static void +priv_zap_event (RakiaMediaSession *self) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (self); + + if (priv->saved_event[0]) + { + nua_event_data_t const *ev_data = nua_event_data (priv->saved_event); + g_assert (ev_data != NULL); + WARNING ("zapping unhandled saved event '%s'", nua_event_name (ev_data->e_event)); + nua_destroy_event (priv->saved_event); + } +} + +void +rakia_media_session_receive_invite (RakiaMediaSession *self) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (self); + + g_return_if_fail (priv->state == TPSIP_MEDIA_SESSION_STATE_CREATED); + g_return_if_fail (priv->nua_op != NULL); + + priv->remote_initiated = TRUE; + + nua_respond (priv->nua_op, SIP_180_RINGING, TAG_END()); + + rakia_media_session_change_state (self, TPSIP_MEDIA_SESSION_STATE_INVITE_RECEIVED); +} + +void +rakia_media_session_receive_reinvite (RakiaMediaSession *self) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (self); + + /* Check for permitted state transitions */ + switch (priv->state) + { + case TPSIP_MEDIA_SESSION_STATE_ACTIVE: + case TPSIP_MEDIA_SESSION_STATE_RESPONSE_RECEIVED: + break; + case TPSIP_MEDIA_SESSION_STATE_REINVITE_PENDING: + g_source_remove (priv->glare_timer_id); + break; + default: + g_return_if_reached (); + } + + priv_save_event (self); + + rakia_media_session_change_state (self, TPSIP_MEDIA_SESSION_STATE_REINVITE_RECEIVED); +} + +void +rakia_media_session_accept (RakiaMediaSession *self) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (self); + + if (priv->accepted) + return; + + SESSION_DEBUG (self, "accepting the session"); + + priv->accepted = TRUE; + + /* Apply the pending send flags */ + priv_apply_streams_pending_direction (self, + TP_MEDIA_STREAM_PENDING_LOCAL_SEND | + TP_MEDIA_STREAM_PENDING_REMOTE_SEND); + + /* Will change session state to active when streams are ready */ + priv_request_response_step (self); +} + +void +rakia_media_session_respond (RakiaMediaSession *self, + gint status, + const char *message) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (self); + + SESSION_DEBUG (self, "responding: %03d %s", status, message ? message : ""); + + if (message != NULL && !message[0]) + message = NULL; + + if (priv->nua_op) + nua_respond (priv->nua_op, status, message, TAG_END()); +} + +gboolean rakia_media_session_is_accepted (RakiaMediaSession *self) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (self); + return priv->accepted; +} + +static gboolean +priv_glare_retry (gpointer session) +{ + RakiaMediaSession *self = session; + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (self); + + SESSION_DEBUG (self, "glare resolution interval is over"); + + if (priv->state == TPSIP_MEDIA_SESSION_STATE_REINVITE_PENDING) + priv_session_invite (self, TRUE); + + /* Reap the timer */ + priv->glare_timer_id = 0; + return FALSE; +} + +void +rakia_media_session_resolve_glare (RakiaMediaSession *self) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (self); + guint interval; + + if (priv->state != TPSIP_MEDIA_SESSION_STATE_REINVITE_SENT) + { + SESSION_DEBUG (self, "glare resolution triggered in unexpected state"); + return; + } + + /* + * Set the grace interval accordinlgly to RFC 3261 section 14.1: + * + * 1. If the UAC is the owner of the Call-ID of the dialog ID + * (meaning it generated the value), T has a randomly chosen value + * between 2.1 and 4 seconds in units of 10 ms. + * 2. If the UAC is not the owner of the Call-ID of the dialog ID, T + * has a randomly chosen value of between 0 and 2 seconds in units + * of 10 ms. + */ + if (priv->pending_offer) + interval = 0; /* cut short, we have new things to negotiate */ + else if (priv->remote_initiated) + interval = g_random_int_range (0, 200) * 10; + else + interval = g_random_int_range (210, 400) * 10; + + if (priv->glare_timer_id != 0) + g_source_remove (priv->glare_timer_id); + + priv->glare_timer_id = g_timeout_add (interval, priv_glare_retry, self); + + SESSION_DEBUG (self, "glare resolution interval %u msec", interval); + + rakia_media_session_change_state ( + self, TPSIP_MEDIA_SESSION_STATE_REINVITE_PENDING); +} + +static RakiaMediaStream * +rakia_media_session_get_stream (RakiaMediaSession *self, + guint stream_id, + GError **error) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (self); + RakiaMediaStream *stream; + + g_assert (priv->streams != NULL); + + if (stream_id >= priv->streams->len) + { + g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "stream ID %u is invalid", stream_id); + return NULL; + } + + stream = g_ptr_array_index (priv->streams, stream_id); + + if (stream == NULL) + { + g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "stream %u does not exist", stream_id); + return NULL; + } + + return stream; +} + +TpLocalHoldState +rakia_media_session_get_hold_state (RakiaMediaSession *self) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (self); + return priv->hold_state; +} + +static gboolean +rakia_media_session_is_local_hold_ongoing (RakiaMediaSession *self) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (self); + return (priv->hold_state == TP_LOCAL_HOLD_STATE_HELD + || priv->hold_state == TP_LOCAL_HOLD_STATE_PENDING_HOLD); +} + +static void +priv_initiate_hold (RakiaMediaSession *self, + gboolean hold, + TpLocalHoldStateReason reason) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (self); + gboolean stream_hold_requested = FALSE; + RakiaMediaStream *stream; + guint i; + + DEBUG("enter"); + + if (hold) + { + if (priv->hold_state == TP_LOCAL_HOLD_STATE_HELD + || priv->hold_state == TP_LOCAL_HOLD_STATE_PENDING_HOLD) + { + MESSAGE ("redundant hold request"); + return; + } + } + else + { + if (priv->hold_state == TP_LOCAL_HOLD_STATE_UNHELD + || priv->hold_state == TP_LOCAL_HOLD_STATE_PENDING_UNHOLD) + { + MESSAGE ("redundant unhold request"); + return; + } + } + + /* Emit the hold notification for every stream that needs it */ + for (i = 0; i < priv->streams->len; i++) + { + stream = g_ptr_array_index(priv->streams, i); + if (stream != NULL + && rakia_media_stream_request_hold_state (stream, hold)) + stream_hold_requested = TRUE; + } + + if (stream_hold_requested) + { + priv->hold_state = hold? TP_LOCAL_HOLD_STATE_PENDING_HOLD + : TP_LOCAL_HOLD_STATE_PENDING_UNHOLD; + } + else + { + /* There were no streams to flip, short cut to the final state */ + priv->hold_state = hold? TP_LOCAL_HOLD_STATE_HELD + : TP_LOCAL_HOLD_STATE_UNHELD; + } + priv->hold_reason = reason; + + tp_svc_channel_interface_hold_emit_hold_state_changed (priv->channel, + priv->hold_state, reason); +} + +static void +priv_finalize_hold (RakiaMediaSession *self) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (self); + RakiaMediaStream *stream; + TpLocalHoldState final_hold_state; + guint hold_mask; + guint unhold_mask; + guint i; + gboolean held = FALSE; + + DEBUG("enter"); + + switch (priv->hold_state) + { + case TP_LOCAL_HOLD_STATE_PENDING_HOLD: + held = TRUE; + break; + case TP_LOCAL_HOLD_STATE_PENDING_UNHOLD: + held = FALSE; + break; + default: + /* Streams changed state without request, signal this to the client. + * All streams should have the same hold state at this point, + * so just query one of them for the current hold state */ + stream = NULL; + for (i = 0; i < priv->streams->len; i++) + { + stream = g_ptr_array_index(priv->streams, i); + if (stream != NULL) + break; + } + g_return_if_fail (stream != NULL); + + g_object_get (stream, "hold-state", &held, NULL); + } + + if (held) + { + final_hold_state = TP_LOCAL_HOLD_STATE_HELD; + hold_mask = TP_MEDIA_STREAM_DIRECTION_SEND; + unhold_mask = 0; + } + else + { + final_hold_state = TP_LOCAL_HOLD_STATE_UNHELD; + hold_mask = TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL; + unhold_mask = TP_MEDIA_STREAM_DIRECTION_RECEIVE; + } + + priv->hold_state = final_hold_state; + tp_svc_channel_interface_hold_emit_hold_state_changed (priv->channel, + final_hold_state, priv->hold_reason); + + /* Set stream directions accordingly to the achieved hold state */ + for (i = 0; i < priv->streams->len; i++) + { + stream = g_ptr_array_index(priv->streams, i); + if (stream != NULL) + { + guint direction = rakia_media_stream_get_requested_direction (stream); + direction &= hold_mask; + direction |= unhold_mask; + rakia_media_stream_set_direction (stream, + direction, + TP_MEDIA_STREAM_PENDING_REMOTE_SEND + | TP_MEDIA_STREAM_PENDING_LOCAL_SEND); + } + } +} + +void +rakia_media_session_request_hold (RakiaMediaSession *self, + gboolean hold) +{ + priv_initiate_hold (self, + hold, + TP_LOCAL_HOLD_STATE_REASON_REQUESTED); +} + +gboolean +rakia_media_session_start_telephony_event (RakiaMediaSession *self, + guint stream_id, + guchar event, + GError **error) +{ + RakiaMediaStream *stream; + + stream = rakia_media_session_get_stream (self, stream_id, error); + if (stream == NULL) + return FALSE; + + if (rakia_media_stream_get_media_type (stream) != TP_MEDIA_STREAM_TYPE_AUDIO) + { + g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "non-audio stream %u does not support telephony events", stream_id); + return FALSE; + } + + DEBUG("starting telephony event %u on stream %u", (guint) event, stream_id); + + rakia_media_stream_start_telephony_event (stream, event); + + return TRUE; +} + +gboolean +rakia_media_session_stop_telephony_event (RakiaMediaSession *self, + guint stream_id, + GError **error) +{ + RakiaMediaStream *stream; + + stream = rakia_media_session_get_stream (self, stream_id, error); + if (stream == NULL) + return FALSE; + + if (rakia_media_stream_get_media_type (stream) != TP_MEDIA_STREAM_TYPE_AUDIO) + { + g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "non-audio stream %u does not support telephony events; spurious use of the stop event?", stream_id); + return FALSE; + } + + DEBUG("stopping the telephony event on stream %u", stream_id); + + rakia_media_stream_stop_telephony_event (stream); + + return TRUE; +} + +gint +rakia_media_session_rate_native_transport (RakiaMediaSession *session, + const GValue *transport) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + gint result = 0; + gchar *address = NULL; + guint proto = TP_MEDIA_STREAM_BASE_PROTO_UDP; + + dbus_g_type_struct_get (transport, + 1, &address, + 3, &proto, + G_MAXUINT); + + g_assert (address != NULL); + + if (proto != TP_MEDIA_STREAM_BASE_PROTO_UDP) + result = -1; + /* XXX: this will not work properly when IPv6 support comes */ + else if (priv->local_ip_address != NULL + && strcmp (address, priv->local_ip_address) == 0) + result = 1; + + g_free (address); + + return result; +} + +static void +priv_session_set_streams_playing (RakiaMediaSession *session, gboolean playing) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + RakiaMediaStream *stream; + guint i; + + for (i = 0; i < priv->streams->len; i++) + { + stream = g_ptr_array_index(priv->streams, i); + if (stream != NULL) + rakia_media_stream_set_playing (stream, playing); + } +} + +static void +priv_local_media_changed (RakiaMediaSession *session) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + + switch (priv->state) + { + case TPSIP_MEDIA_SESSION_STATE_CREATED: + /* If all streams are ready, send an offer now */ + priv_request_response_step (session); + break; + case TPSIP_MEDIA_SESSION_STATE_INVITE_RECEIVED: + case TPSIP_MEDIA_SESSION_STATE_REINVITE_RECEIVED: + /* The changes to existing streams will be included in the + * eventual answer (FIXME: implement postponed direction changes, + * which are applied after the remote offer has been processed). + * Check, however, if there are new streams not present in the + * remote offer, that will need another offer-answer round */ + if (priv->remote_stream_count < priv->streams->len) + priv->pending_offer = TRUE; + break; + case TPSIP_MEDIA_SESSION_STATE_INVITE_SENT: + case TPSIP_MEDIA_SESSION_STATE_REINVITE_SENT: + case TPSIP_MEDIA_SESSION_STATE_RESPONSE_RECEIVED: + /* Cannot send another offer right now */ + priv->pending_offer = TRUE; + break; + case TPSIP_MEDIA_SESSION_STATE_ACTIVE: + /* Check if we are allowed to send re-INVITES */ + { + gboolean immutable_streams = FALSE; + g_object_get (priv->channel, + "immutable-streams", &immutable_streams, + NULL); + if (immutable_streams) { + g_message ("sending of a local media update disabled by parameter 'immutable-streams'"); + break; + } + } + /* Fall through to the next case */ + case TPSIP_MEDIA_SESSION_STATE_REINVITE_PENDING: + if (priv->local_non_ready == 0) + priv_session_invite (session, TRUE); + else + priv->pending_offer = TRUE; + break; + default: + g_assert_not_reached(); + } +} + +static void +priv_update_remote_hold (RakiaMediaSession *session) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + RakiaMediaStream *stream; + gboolean has_streams = FALSE; + gboolean remote_held = TRUE; + guint direction; + guint i; + + /* The call is remotely unheld if there's at least one sending stream */ + for (i = 0; i < priv->streams->len; i++) + { + stream = g_ptr_array_index(priv->streams, i); + if (stream != NULL) + { + direction = rakia_media_stream_get_requested_direction (stream); + + if ((direction & TP_MEDIA_STREAM_DIRECTION_SEND) != 0) + remote_held = FALSE; + + has_streams = TRUE; + } + } + + if (!has_streams) + return; + + DEBUG("the session is remotely %s", remote_held? "held" : "unheld"); + + if (remote_held) + rakia_media_channel_change_call_state (priv->channel, + priv->peer, + TP_CHANNEL_CALL_STATE_HELD, + 0); + else + rakia_media_channel_change_call_state (priv->channel, + priv->peer, + 0, + TP_CHANNEL_CALL_STATE_HELD); +} + +gchar * +rakia_sdp_get_string_attribute (const sdp_attribute_t *attrs, const char *name) +{ + sdp_attribute_t *attr; + + attr = sdp_attribute_find (attrs, name); + if (attr == NULL) + return NULL; + + return g_strdup (attr->a_value); +} + +static gboolean +priv_update_remote_media (RakiaMediaSession *session, gboolean authoritative) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + const sdp_session_t *sdp = priv->remote_sdp; + const sdp_media_t *media; + gboolean has_supported_media = FALSE; + guint direction_up_mask; + guint pending_send_mask; + guint i; + + g_return_val_if_fail (sdp != NULL, FALSE); + + /* Update the session-wide parameters + * before updating streams' media */ + + priv->remote_ptime = rakia_sdp_get_string_attribute ( + sdp->sdp_attributes, "ptime"); + priv->remote_max_ptime = rakia_sdp_get_string_attribute ( + sdp->sdp_attributes, "maxptime"); + + priv->rtcp_enabled = !rakia_sdp_rtcp_bandwidth_throttled ( + sdp->sdp_bandwidths); + + /* + * Do not allow: + * 1) an answer to bump up directions beyond what's been offered; + * 2) an offer to remove the local hold. + */ + if (authoritative) + direction_up_mask + = rakia_media_session_is_local_hold_ongoing (session) + ? TP_MEDIA_STREAM_DIRECTION_SEND + : TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL; + else + direction_up_mask = 0; + + /* A remote media requesting to enable sending would need local approval. + * Also, if there have been any local media updates pending a re-INVITE, + * keep or bump the pending remote send flag on the streams: it will + * be resolved in the next re-INVITE transaction */ + pending_send_mask = TP_MEDIA_STREAM_PENDING_LOCAL_SEND; + if (priv->pending_offer) + pending_send_mask |= TP_MEDIA_STREAM_PENDING_REMOTE_SEND; + + media = sdp->sdp_media; + + /* note: for each session, we maintain an ordered list of + * streams (SDP m-lines) which are matched 1:1 to + * the streams of the remote SDP */ + + for (i = 0; media != NULL; media = media->m_next, i++) + { + RakiaMediaStream *stream = NULL; + guint media_type; + + media_type = rakia_tp_media_type (media->m_type); + + if (i >= priv->streams->len) + stream = rakia_media_session_add_stream ( + session, + media_type, + rakia_media_stream_direction_from_remote_media (media), + FALSE); + else + stream = g_ptr_array_index(priv->streams, i); + + /* note: it is ok for the stream to be NULL (unsupported media type) */ + if (stream == NULL) + continue; + + DEBUG("setting remote SDP for stream %u", i); + + if (media->m_rejected) + { + DEBUG("the stream has been rejected, closing"); + } + else if (rakia_media_stream_get_media_type (stream) != media_type) + { + /* XXX: close this stream and create a new one in its place? */ + WARNING ("The peer has changed the media type, don't know what to do"); + } + else if (rakia_media_stream_set_remote_media (stream, + media, + direction_up_mask, + pending_send_mask)) + { + has_supported_media = TRUE; + continue; + } + + /* There have been problems with the stream update, kill the stream */ + rakia_media_stream_close (stream); + } + g_assert(media == NULL); + g_assert(i <= priv->streams->len); + g_assert(!authoritative || i == priv->remote_stream_count); + + if (i < priv->streams->len && !priv->pending_offer) + { + /* + * It's not defined what we should do if there are previously offered + * streams not accounted in the remote SDP, in violation of RFC 3264. + * Closing them off serves resource preservation and gives better + * clue to the client as to the real state of the session. + * Note that this situation is masked if any local media updates + * have been requested and are pending until the present remote session + * answer is received and applied. In such a case, we'll issue a new offer + * at the closest available time, with the "overhanging" stream entries + * intact. + */ + do + { + RakiaMediaStream *stream; + stream = g_ptr_array_index(priv->streams, i); + if (stream != NULL) + { + MESSAGE ("closing a mismatched stream %u", i); + rakia_media_stream_close (stream); + } + } + while (++i < priv->streams->len); + } + + if (has_supported_media) + priv_update_remote_hold (session); + + DEBUG("exit"); + + return has_supported_media; +} + +static void +priv_session_rollback (RakiaMediaSession *session) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + + DEBUG("enter"); + + if (priv->remote_sdp != NULL) + { + priv->remote_sdp = NULL; + g_assert (priv->home != NULL); + su_home_unref (priv->home); + priv->home = NULL; + } + if (priv->backup_remote_sdp == NULL) + { + rakia_media_session_terminate (session); + return; + } + + /* restore remote SDP from the backup */ + priv->remote_sdp = priv->backup_remote_sdp; + g_assert (priv->backup_home != NULL); + priv->home = priv->backup_home; + priv->backup_remote_sdp = NULL; + priv->backup_home = NULL; + + priv_update_remote_media (session, FALSE); + + if (priv->saved_event[0]) + { + nua_respond (priv->nua_op, SIP_488_NOT_ACCEPTABLE, + NUTAG_WITH(nua_saved_event_request (priv->saved_event)), + TAG_END()); + nua_destroy_event (priv->saved_event); + } + else + { + nua_respond (priv->nua_op, SIP_488_NOT_ACCEPTABLE, + TAG_END()); + } + + rakia_media_session_change_state (session, TPSIP_MEDIA_SESSION_STATE_ACTIVE); +} + +static gboolean +priv_session_local_sdp (RakiaMediaSession *session, + GString *user_sdp, + gboolean authoritative) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + gboolean has_supported_media = FALSE; + guint len; + guint i; + + g_return_val_if_fail (priv->local_non_ready == 0, FALSE); + + len = priv->streams->len; + if (!authoritative && len > priv->remote_stream_count) + { + len = priv->remote_stream_count; + DEBUG("clamped response to %u streams seen in the offer", len); + } + + g_string_append (user_sdp, "v=0\r\n"); + + for (i = 0; i < len; i++) + { + RakiaMediaStream *stream = g_ptr_array_index (priv->streams, i); + if (stream) + { + user_sdp = g_string_append (user_sdp, + rakia_media_stream_local_sdp (stream)); + has_supported_media = TRUE; + } + else + { + user_sdp = g_string_append (user_sdp, "m=audio 0 RTP/AVP 0\r\n"); + } + } + + return has_supported_media; +} + +static void +priv_session_invite (RakiaMediaSession *session, gboolean reinvite) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + GString *user_sdp; + + DEBUG("enter"); + + g_return_if_fail (priv->nua_op != NULL); + + user_sdp = g_string_new (NULL); + + if (priv_session_local_sdp (session, user_sdp, TRUE)) + { + /* We need to be prepared to receive media right after the + * offer is sent, so we must set the streams to playing */ + priv_session_set_streams_playing (session, TRUE); + + nua_invite (priv->nua_op, + SOATAG_USER_SDP_STR(user_sdp->str), + SOATAG_RTP_SORT(SOA_RTP_SORT_REMOTE), + SOATAG_RTP_SELECT(SOA_RTP_SELECT_ALL), + NUTAG_AUTOANSWER(0), + TAG_IF(reinvite, + NUTAG_INVITE_TIMER (TPSIP_REINVITE_TIMEOUT)), + TAG_END()); + priv->pending_offer = FALSE; + + rakia_media_session_change_state ( + session, + reinvite? TPSIP_MEDIA_SESSION_STATE_REINVITE_SENT + : TPSIP_MEDIA_SESSION_STATE_INVITE_SENT); + } + else + WARNING ("cannot send a valid SDP offer, are there no streams?"); + + g_string_free (user_sdp, TRUE); +} + +static void +priv_session_respond (RakiaMediaSession *session) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + GString *user_sdp; + + g_return_if_fail (priv->nua_op != NULL); + + user_sdp = g_string_new (NULL); + + if (priv_session_local_sdp (session, user_sdp, FALSE)) + { + msg_t *msg; + + /* We need to be prepared to receive media right after the + * answer is sent, so we must set the streams to playing */ + priv_session_set_streams_playing (session, TRUE); + + msg = (priv->saved_event[0]) + ? nua_saved_event_request (priv->saved_event) : NULL; + + nua_respond (priv->nua_op, SIP_200_OK, + TAG_IF(msg, NUTAG_WITH(msg)), + SOATAG_USER_SDP_STR (user_sdp->str), + SOATAG_RTP_SORT(SOA_RTP_SORT_REMOTE), + SOATAG_RTP_SELECT(SOA_RTP_SELECT_ALL), + NUTAG_AUTOANSWER(0), + TAG_END()); + + if (priv->saved_event[0]) + nua_destroy_event (priv->saved_event); + + rakia_media_session_change_state (session, TPSIP_MEDIA_SESSION_STATE_ACTIVE); + } + else + { + WARNING ("cannot respond with a valid SDP answer, were all streams closed?"); + + priv_session_rollback (session); + } + + g_string_free (user_sdp, TRUE); +} + +static gboolean +priv_is_codec_intersect_pending (RakiaMediaSession *session) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + guint i; + + for (i = 0; i < priv->streams->len; i++) + { + RakiaMediaStream *stream = g_ptr_array_index (priv->streams, i); + if (stream != NULL + && rakia_media_stream_is_codec_intersect_pending (stream)) + return TRUE; + } + + return FALSE; +} + +/** + * Sends requests and responses with an outbound offer/answer + * if all streams of the session are prepared. + * + * Following inputs are considered in decision making: + * - state of the session (is remote INVITE being handled) + * - status of local streams (set up with stream-engine) + * - whether session is locally accepted + */ +static void +priv_request_response_step (RakiaMediaSession *session) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + + if (priv->local_non_ready != 0) + { + DEBUG("there are local streams not ready, postponed"); + return; + } + + switch (priv->state) + { + case TPSIP_MEDIA_SESSION_STATE_CREATED: + priv_session_invite (session, FALSE); + break; + case TPSIP_MEDIA_SESSION_STATE_RESPONSE_RECEIVED: + if (priv->accepted + && !priv_is_codec_intersect_pending (session)) + rakia_media_session_change_state (session, + TPSIP_MEDIA_SESSION_STATE_ACTIVE); + break; + case TPSIP_MEDIA_SESSION_STATE_INVITE_RECEIVED: + /* TODO: if the call has not yet been accepted locally + * and the remote endpoint supports 100rel, send them + * an early session answer in a reliable 183 response */ + if (priv->accepted + && !priv_is_codec_intersect_pending (session)) + priv_session_respond (session); + break; + case TPSIP_MEDIA_SESSION_STATE_REINVITE_RECEIVED: + if (!priv_is_codec_intersect_pending (session)) + priv_session_respond (session); + break; + case TPSIP_MEDIA_SESSION_STATE_ACTIVE: + case TPSIP_MEDIA_SESSION_STATE_REINVITE_PENDING: + if (priv->pending_offer) + priv_session_invite (session, TRUE); + break; + default: + SESSION_DEBUG (session, "no action taken in the current state"); + } +} + +static void +priv_stream_close_cb (RakiaMediaStream *stream, + RakiaMediaSession *session) +{ + RakiaMediaSessionPrivate *priv; + guint id; + + DEBUG("enter"); + + priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + + id = rakia_media_stream_get_id (stream); + g_return_if_fail (g_ptr_array_index(priv->streams, id) == stream); + + if (!rakia_media_stream_is_local_ready (stream)) + { + g_assert (priv->local_non_ready > 0); + --priv->local_non_ready; + DEBUG("stream wasn't ready, decrement the local non ready counter to %d", priv->local_non_ready); + } + + g_object_unref (stream); + + g_ptr_array_index(priv->streams, id) = NULL; + + tp_svc_channel_type_streamed_media_emit_stream_removed (priv->channel, id); +} + +static void priv_stream_ready_cb (RakiaMediaStream *stream, + RakiaMediaSession *session) +{ + RakiaMediaSessionPrivate *priv; + + DEBUG ("enter"); + + priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + + g_assert (priv->local_non_ready > 0); + --priv->local_non_ready; + + priv_request_response_step (session); +} + +static void priv_stream_supported_codecs_cb (RakiaMediaStream *stream, + guint num_codecs, + RakiaMediaSession *session) +{ + RakiaMediaSessionPrivate *priv; + + priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + + g_assert (!rakia_media_stream_is_codec_intersect_pending (stream)); + + if (num_codecs == 0) + { + /* This remote media description got no codec intersection. */ + switch (priv->state) + { + case TPSIP_MEDIA_SESSION_STATE_RESPONSE_RECEIVED: + case TPSIP_MEDIA_SESSION_STATE_INVITE_RECEIVED: + DEBUG("no codec intersection, closing the stream"); + rakia_media_stream_close (stream); + break; + case TPSIP_MEDIA_SESSION_STATE_REINVITE_RECEIVED: + /* In this case, we have the stream negotiated already, + * and we don't want to close it just because the remote party + * offers a different set of codecs. + * Roll back the whole session to the previously negotiated state. */ + priv_session_rollback (session); + return; + case TPSIP_MEDIA_SESSION_STATE_ACTIVE: + /* We've most likely rolled back from + * TPSIP_MEDIA_SESSION_STATE_REINVITE_RECEIVED, + * but we may receive more than one empty codec intersection + * in the session, so we ignore the rest */ + return; + default: + g_assert_not_reached(); + } + } + + priv_request_response_step (session); +} + +static void +priv_stream_state_changed_cb (RakiaMediaStream *stream, + guint state, + RakiaMediaChannel *channel) +{ + g_assert (TPSIP_IS_MEDIA_CHANNEL (channel)); + tp_svc_channel_type_streamed_media_emit_stream_state_changed( + channel, + rakia_media_stream_get_id (stream), state); +} + +static void +priv_stream_direction_changed_cb (RakiaMediaStream *stream, + guint direction, + guint pending_send_flags, + RakiaMediaChannel *channel) +{ + g_assert (TPSIP_IS_MEDIA_CHANNEL (channel)); + tp_svc_channel_type_streamed_media_emit_stream_direction_changed ( + channel, + rakia_media_stream_get_id (stream), direction, pending_send_flags); +} + +static void +priv_stream_hold_state_cb (RakiaMediaStream *stream, + GParamSpec *pspec, + RakiaMediaSession *session) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (session); + gboolean hold; + guint i; + + /* Determine the hold state all streams shall come to */ + switch (priv->hold_state) + { + case TP_LOCAL_HOLD_STATE_PENDING_HOLD: + hold = TRUE; + break; + case TP_LOCAL_HOLD_STATE_PENDING_UNHOLD: + hold = FALSE; + break; + default: + MESSAGE ("unexpected hold state change from a stream"); + + /* Try to follow the changes and report the resulting hold state */ + g_object_get (stream, "hold-state", &hold, NULL); + priv->hold_reason = TP_LOCAL_HOLD_STATE_REASON_NONE; + } + + /* Check if all streams have reached the desired hold state */ + for (i = 0; i < priv->streams->len; i++) + { + stream = g_ptr_array_index (priv->streams, i); + if (stream != NULL) + { + gboolean stream_held = FALSE; + g_object_get (stream, "hold-state", &stream_held, NULL); + if ((!stream_held) != (!hold)) + { + DEBUG("hold/unhold not complete yet"); + return; + } + } + } + + priv_finalize_hold (session); +} + +static void +priv_stream_unhold_failure_cb (RakiaMediaStream *stream, + RakiaMediaSession *session) +{ + priv_initiate_hold (session, + TRUE, + TP_LOCAL_HOLD_STATE_REASON_RESOURCE_NOT_AVAILABLE); +} + +RakiaMediaStream* +rakia_media_session_add_stream (RakiaMediaSession *self, + guint media_type, + TpMediaStreamDirection direction, + gboolean created_locally) +{ + RakiaMediaSessionPrivate *priv = TPSIP_MEDIA_SESSION_GET_PRIVATE (self); + RakiaMediaStream *stream = NULL; + + DEBUG ("enter"); + + if (rakia_media_session_supports_media_type (media_type)) { + guint stream_id; + gchar *object_path; + guint pending_send_flags; + + stream_id = priv->streams->len; + object_path = g_strdup_printf ("%s/MediaStream%u", + priv->object_path, + stream_id); + pending_send_flags = created_locally + ? TP_MEDIA_STREAM_PENDING_REMOTE_SEND + : TP_MEDIA_STREAM_PENDING_LOCAL_SEND; + + if (!created_locally) + direction &= ~TP_MEDIA_STREAM_DIRECTION_SEND; + + if (rakia_media_session_is_local_hold_ongoing (self)) + direction &= ~TP_MEDIA_STREAM_DIRECTION_RECEIVE; + + stream = g_object_new (TPSIP_TYPE_MEDIA_STREAM, + "dbus-daemon", priv->dbus_daemon, + "media-session", self, + "media-type", media_type, + "object-path", object_path, + "id", stream_id, + "direction", direction, + "pending-send-flags", pending_send_flags, + "created-locally", created_locally, + NULL); + + g_free (object_path); + + g_signal_connect (stream, "close", + G_CALLBACK (priv_stream_close_cb), + self); + g_signal_connect (stream, "ready", + G_CALLBACK (priv_stream_ready_cb), + self); + g_signal_connect (stream, "supported-codecs", + G_CALLBACK (priv_stream_supported_codecs_cb), + self); + g_signal_connect (stream, "state-changed", + G_CALLBACK (priv_stream_state_changed_cb), + priv->channel); + g_signal_connect (stream, "direction-changed", + G_CALLBACK (priv_stream_direction_changed_cb), + priv->channel); + g_signal_connect_swapped (stream, "local-media-updated", + G_CALLBACK (priv_local_media_changed), + self); + g_signal_connect (stream, "notify::hold-state", + G_CALLBACK (priv_stream_hold_state_cb), + self); + g_signal_connect (stream, "unhold-failure", + G_CALLBACK (priv_stream_unhold_failure_cb), + self); + + g_assert (priv->local_non_ready >= 0); + ++priv->local_non_ready; + + if (priv->se_ready) + priv_emit_new_stream (self, stream); + + tp_svc_channel_type_streamed_media_emit_stream_added (priv->channel, + stream_id, + priv->peer, + media_type); + if (direction != TP_MEDIA_STREAM_DIRECTION_RECEIVE + || pending_send_flags != TP_MEDIA_STREAM_PENDING_LOCAL_SEND) + { + tp_svc_channel_type_streamed_media_emit_stream_direction_changed ( + priv->channel, + stream_id, + direction, + pending_send_flags); + } + } + + /* note: we add an entry even for unsupported media types */ + g_ptr_array_add (priv->streams, stream); + + DEBUG ("exit"); + + return stream; +} + +static void +session_handler_iface_init (gpointer g_iface, gpointer iface_data) +{ + TpSvcMediaSessionHandlerClass *klass = (TpSvcMediaSessionHandlerClass *)g_iface; + +#define IMPLEMENT(x) tp_svc_media_session_handler_implement_##x (\ + klass, (tp_svc_media_session_handler_##x##_impl) rakia_media_session_##x) + IMPLEMENT(error); + IMPLEMENT(ready); +#undef IMPLEMENT +} + +/* Checks if RTCP is not disabled with bandwidth modifiers + * as described in RFC 3556 */ +gboolean +rakia_sdp_rtcp_bandwidth_throttled (const sdp_bandwidth_t *b) +{ + const sdp_bandwidth_t *b_RS = NULL; + const sdp_bandwidth_t *b_RR = NULL; + + while (b != NULL) + { + if (b->b_modifier_name != NULL) + { + if (strcmp (b->b_modifier_name, "RS") == 0) + b_RS = b; + else if (strcmp (b->b_modifier_name, "RR") == 0) + b_RR = b; + } + b = b->b_next; + } + + return (b_RS != NULL && b_RS->b_value == 0 + && b_RR != NULL && b_RR->b_value == 0); +} diff --git a/rakia/media-session.h b/rakia/media-session.h new file mode 100644 index 0000000..158e23b --- /dev/null +++ b/rakia/media-session.h @@ -0,0 +1,143 @@ +/* + * sip-media-session.h - Header for RakiaMediaSession + * Copyright (C) 2005 Collabora Ltd. + * Copyright (C) 2005-2010 Nokia Corporation + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __TPSIP_MEDIA_SESSION_H__ +#define __TPSIP_MEDIA_SESSION_H__ + +#include <rakia/media-stream.h> + +#include <glib-object.h> +#include <telepathy-glib/handle.h> +#include <sofia-sip/sdp.h> + +G_BEGIN_DECLS + +typedef enum { + TPSIP_MEDIA_SESSION_STATE_CREATED = 0, + TPSIP_MEDIA_SESSION_STATE_INVITE_SENT, + TPSIP_MEDIA_SESSION_STATE_INVITE_RECEIVED, + TPSIP_MEDIA_SESSION_STATE_RESPONSE_RECEIVED, + TPSIP_MEDIA_SESSION_STATE_ACTIVE, + TPSIP_MEDIA_SESSION_STATE_REINVITE_SENT, + TPSIP_MEDIA_SESSION_STATE_REINVITE_RECEIVED, + TPSIP_MEDIA_SESSION_STATE_REINVITE_PENDING, + TPSIP_MEDIA_SESSION_STATE_ENDED +} RakiaMediaSessionState; + +typedef struct _RakiaMediaSession RakiaMediaSession; +typedef struct _RakiaMediaSessionClass RakiaMediaSessionClass; + +struct _RakiaMediaSessionClass { + GObjectClass parent_class; +}; + +struct _RakiaMediaSession { + GObject parent; +}; + +GType rakia_media_session_get_type(void); + +/* TYPE MACROS */ +#define TPSIP_TYPE_MEDIA_SESSION \ + (rakia_media_session_get_type()) +#define TPSIP_MEDIA_SESSION(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), TPSIP_TYPE_MEDIA_SESSION, RakiaMediaSession)) +#define TPSIP_MEDIA_SESSION_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), TPSIP_TYPE_MEDIA_SESSION, RakiaMediaSessionClass)) +#define TPSIP_IS_MEDIA_SESSION(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), TPSIP_TYPE_MEDIA_SESSION)) +#define TPSIP_IS_MEDIA_SESSION_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), TPSIP_TYPE_MEDIA_SESSION)) +#define TPSIP_MEDIA_SESSION_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), TPSIP_TYPE_MEDIA_SESSION, RakiaMediaSessionClass)) + +/*********************************************************************** + * Additional declarations (not based on generated templates) + ***********************************************************************/ + +TpHandle rakia_media_session_get_peer (RakiaMediaSession *session); +void rakia_media_session_terminate (RakiaMediaSession *session); +RakiaMediaSessionState rakia_media_session_get_state (RakiaMediaSession *session); +void rakia_media_session_change_state (RakiaMediaSession *session, + RakiaMediaSessionState new_state); +gboolean rakia_media_session_set_remote_media (RakiaMediaSession *chan, + const sdp_session_t* r_sdp); +RakiaMediaStream* rakia_media_session_add_stream (RakiaMediaSession *self, + guint media_type, + TpMediaStreamDirection direction, + gboolean created_locally); +gboolean rakia_media_session_request_streams (RakiaMediaSession *session, + const GArray *media_types, + GPtrArray *ret, + GError **error); +gboolean rakia_media_session_remove_streams (RakiaMediaSession *session, + const GArray *stream_ids, + GError **error); +void rakia_media_session_list_streams (RakiaMediaSession *session, + GPtrArray *ret); +gboolean rakia_media_session_request_stream_direction (RakiaMediaSession *session, + guint stream_id, + guint direction, + GError **error); +void rakia_media_session_receive_invite (RakiaMediaSession *self); +void rakia_media_session_receive_reinvite (RakiaMediaSession *self); +void rakia_media_session_accept (RakiaMediaSession *self); +void rakia_media_session_respond (RakiaMediaSession *self, + gint status, + const char *message); +gboolean rakia_media_session_is_accepted (RakiaMediaSession *self); +void rakia_media_session_resolve_glare (RakiaMediaSession *self); + +TpLocalHoldState rakia_media_session_get_hold_state (RakiaMediaSession *session); +void rakia_media_session_request_hold (RakiaMediaSession *session, + gboolean hold); + +gboolean rakia_media_session_start_telephony_event (RakiaMediaSession *self, + guint stream_id, + guchar event, + GError **error); +gboolean rakia_media_session_stop_telephony_event (RakiaMediaSession *self, + guint stream_id, + GError **error); + +gint rakia_media_session_rate_native_transport (RakiaMediaSession *session, + const GValue *transport); + +gboolean rakia_sdp_rtcp_bandwidth_throttled (const sdp_bandwidth_t *b); + +gchar * rakia_sdp_get_string_attribute (const sdp_attribute_t *attrs, + const char *name); + +#ifdef ENABLE_DEBUG + +#define SESSION_DEBUG(s, ...) rakia_media_session_debug (s, __VA_ARGS__) + +void rakia_media_session_debug (RakiaMediaSession *session, + const gchar *format, ...); + +#else + +#define SESSION_DEBUG(s, ...) + +#endif + +G_END_DECLS + +#endif /* #ifndef __TPSIP_MEDIA_SESSION_H__*/ diff --git a/rakia/media-stream.c b/rakia/media-stream.c new file mode 100644 index 0000000..f8f659e --- /dev/null +++ b/rakia/media-stream.c @@ -0,0 +1,2002 @@ +/* + * sip-media-stream.c - Source for RakiaMediaStream + * Copyright (C) 2006 Collabora Ltd. + * Copyright (C) 2006-2010 Nokia Corporation + * @author Kai Vehmanen <first.surname@nokia.com> + * @author Mikhail Zabaluev <mikhail.zabaluev@nokia.com> + * + * Based on telepathy-gabble implementation (gabble-media-stream). + * @author Ole Andre Vadla Ravnaas <ole.andre.ravnaas@collabora.co.uk> + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" +#include "rakia/media-stream.h" + +#include <dbus/dbus-glib.h> +#include <stdlib.h> +#include <string.h> + +#include <telepathy-glib/dbus.h> +#include <telepathy-glib/enums.h> +#include <telepathy-glib/errors.h> +#include <telepathy-glib/gtypes.h> +#include <telepathy-glib/interfaces.h> +#include <telepathy-glib/svc-generic.h> +#include <telepathy-glib/svc-media-interfaces.h> +#include <telepathy-glib/util.h> + +#include "config.h" + +#include <rakia/codec-param-formats.h> + +#include "rakia/media-session.h" + +#include <sofia-sip/msg_parser.h> + +#include "signals-marshal.h" + +#define DEBUG_FLAG TPSIP_DEBUG_MEDIA +#include "rakia/debug.h" + + +#define same_boolean(old, new) ((!(old)) == (!(new))) + + +static void stream_handler_iface_init (gpointer, gpointer); + +G_DEFINE_TYPE_WITH_CODE(RakiaMediaStream, + rakia_media_stream, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_MEDIA_STREAM_HANDLER, + stream_handler_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_DBUS_PROPERTIES, + tp_dbus_properties_mixin_iface_init); + ) + +/* signal enum */ +enum +{ + SIG_READY, + SIG_SUPPORTED_CODECS, + SIG_STATE_CHANGED, + SIG_DIRECTION_CHANGED, + SIG_LOCAL_MEDIA_UPDATED, + SIG_UNHOLD_FAILURE, + + SIG_LAST_SIGNAL +}; + +static guint signals[SIG_LAST_SIGNAL] = {0}; + +/* properties */ +enum +{ + PROP_MEDIA_SESSION = 1, + PROP_DBUS_DAEMON, + PROP_OBJECT_PATH, + PROP_ID, + PROP_MEDIA_TYPE, + PROP_STATE, + PROP_DIRECTION, + PROP_PENDING_SEND_FLAGS, + PROP_HOLD_STATE, + PROP_CREATED_LOCALLY, + PROP_NAT_TRAVERSAL, + PROP_STUN_SERVERS, + PROP_RELAY_INFO, + LAST_PROPERTY +}; + +static GPtrArray *rakia_media_stream_relay_info_empty = NULL; + +/* private structure */ +typedef struct _RakiaMediaStreamPrivate RakiaMediaStreamPrivate; + +struct _RakiaMediaStreamPrivate +{ + TpDBusDaemon *dbus_daemon; + RakiaMediaSession *session; /* see gobj. prop. 'media-session' */ + gchar *object_path; /* see gobj. prop. 'object-path' */ + guint id; /* see gobj. prop. 'id' */ + guint media_type; /* see gobj. prop. 'media-type' */ + guint state; /* see gobj. prop. 'state' */ + guint direction; /* see gobj. prop. 'direction' */ + guint pending_send_flags; /* see gobj. prop. 'pending-send-flags' */ + gboolean hold_state; /* see gobj. prop. 'hold-state' */ + gboolean created_locally; /* see gobj. prop. 'created-locally' */ + + gchar *stream_sdp; /* SDP description of the stream */ + + GValue native_codecs; /* intersected codec list */ + GValue native_candidates; + + const sdp_media_t *remote_media; /* pointer to the SDP media structure + * owned by the session object */ + + guint remote_candidate_counter; + gchar *remote_candidate_id; + + gchar *native_candidate_id; + + gboolean ready_received; /* our ready method has been called */ + gboolean playing; /* stream set to playing */ + gboolean sending; /* stream set to sending */ + gboolean pending_remote_receive; /* TRUE if remote is to agree to receive media */ + gboolean native_cands_prepared; /* all candidates discovered */ + gboolean native_codecs_prepared; /* all codecs discovered */ + gboolean push_remote_cands_pending; /* SetRemoteCandidates emission is pending */ + gboolean push_remote_codecs_pending; /* SetRemoteCodecs emission is pending */ + gboolean codec_intersect_pending; /* codec intersection is pending */ + gboolean requested_hold_state; /* hold state last requested from the stream handler */ + gboolean dispose_has_run; +}; + +#define TPSIP_MEDIA_STREAM_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), TPSIP_TYPE_MEDIA_STREAM, RakiaMediaStreamPrivate)) + +static void push_remote_codecs (RakiaMediaStream *stream); +static void push_remote_candidates (RakiaMediaStream *stream); +static void push_active_candidate_pair (RakiaMediaStream *stream); +static void priv_update_sending (RakiaMediaStream *stream, + TpMediaStreamDirection direction); +static void priv_update_local_sdp(RakiaMediaStream *stream); +static void priv_generate_sdp (RakiaMediaStream *stream); + +#if 0 +#ifdef ENABLE_DEBUG +static const char *debug_tp_protocols[] = { + "TP_MEDIA_STREAM_PROTO_UDP (0)", + "TP_MEDIA_STREAM_PROTO_TCP (1)" +}; + +static const char *debug_tp_transports[] = { + "TP_MEDIA_STREAM_TRANSPORT_TYPE_LOCAL (0)", + "TP_MEDIA_STREAM_TRANSPORT_TYPE_DERIVED (1)", + "TP_MEDIA_STREAM_TRANSPORT_TYPE_RELAY (2)" +}; +#endif /* ENABLE_DEBUG */ +#endif /* 0 */ + +/*********************************************************************** + * Set: Gobject interface + ***********************************************************************/ + +static void +rakia_media_stream_init (RakiaMediaStream *self) +{ + RakiaMediaStreamPrivate *priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (self); + + priv->playing = FALSE; + priv->sending = FALSE; + + g_value_init (&priv->native_codecs, TP_ARRAY_TYPE_MEDIA_STREAM_HANDLER_CODEC_LIST); + g_value_take_boxed (&priv->native_codecs, + dbus_g_type_specialized_construct (TP_ARRAY_TYPE_MEDIA_STREAM_HANDLER_CODEC_LIST)); + + g_value_init (&priv->native_candidates, TP_ARRAY_TYPE_MEDIA_STREAM_HANDLER_CANDIDATE_LIST); + g_value_take_boxed (&priv->native_candidates, + dbus_g_type_specialized_construct (TP_ARRAY_TYPE_MEDIA_STREAM_HANDLER_CANDIDATE_LIST)); + + priv->native_cands_prepared = FALSE; + priv->native_codecs_prepared = FALSE; + + priv->push_remote_cands_pending = FALSE; + priv->push_remote_codecs_pending = FALSE; +} + +static void +rakia_media_stream_constructed (GObject *obj) +{ + RakiaMediaStreamPrivate *priv = TPSIP_MEDIA_STREAM_GET_PRIVATE ( + TPSIP_MEDIA_STREAM (obj)); + GObjectClass *parent_object_class = + G_OBJECT_CLASS (rakia_media_stream_parent_class); + + /* call base class method */ + if (parent_object_class->constructed != NULL) + parent_object_class->constructed (obj); + + /* XXX: overloading the remote pending send flag to check + * if this is a locally offered stream. The code creating such streams + * always sets the flag, because the remote end is supposed to decide + * whether it wants to send. + * This may look weird during a local hold. However, the pending flag + * will be harmlessly cleared once the offer-answer is complete. */ + if ((priv->direction & TP_MEDIA_STREAM_DIRECTION_SEND) != 0 + && (priv->pending_send_flags & TP_MEDIA_STREAM_PENDING_REMOTE_SEND) != 0) + { + /* Block sending until the stream is remotely accepted */ + priv->pending_remote_receive = TRUE; + } + + /* go for the bus */ + g_assert (TP_IS_DBUS_DAEMON (priv->dbus_daemon)); + tp_dbus_daemon_register_object (priv->dbus_daemon, priv->object_path, obj); +} + +static void +rakia_media_stream_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + RakiaMediaStream *stream = TPSIP_MEDIA_STREAM (object); + RakiaMediaStreamPrivate *priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (stream); + + switch (property_id) + { + case PROP_DBUS_DAEMON: + g_value_set_object (value, priv->dbus_daemon); + break; + case PROP_MEDIA_SESSION: + g_value_set_object (value, priv->session); + break; + case PROP_OBJECT_PATH: + g_value_set_string (value, priv->object_path); + break; + case PROP_ID: + g_value_set_uint (value, priv->id); + break; + case PROP_MEDIA_TYPE: + g_value_set_uint (value, priv->media_type); + break; + case PROP_STATE: + g_value_set_uint (value, priv->state); + break; + case PROP_DIRECTION: + g_value_set_uint (value, priv->direction); + break; + case PROP_PENDING_SEND_FLAGS: + g_value_set_uint (value, priv->pending_send_flags); + break; + case PROP_HOLD_STATE: + g_value_set_boolean (value, priv->hold_state); + break; + case PROP_CREATED_LOCALLY: + g_value_set_boolean (value, priv->created_locally); + break; + case PROP_NAT_TRAVERSAL: + g_value_set_static_string (value, "none"); + break; + case PROP_STUN_SERVERS: + g_return_if_fail (priv->session != NULL); + g_object_get_property (G_OBJECT (priv->session), "stun-servers", value); + break; + case PROP_RELAY_INFO: + g_value_set_static_boxed (value, rakia_media_stream_relay_info_empty); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +rakia_media_stream_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + RakiaMediaStream *stream = TPSIP_MEDIA_STREAM (object); + RakiaMediaStreamPrivate *priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (stream); + + switch (property_id) + { + case PROP_DBUS_DAEMON: + g_assert (priv->dbus_daemon == NULL); /* construct-only */ + priv->dbus_daemon = g_value_dup_object (value); + break; + case PROP_MEDIA_SESSION: + priv->session = g_value_get_object (value); + break; + case PROP_OBJECT_PATH: + g_free (priv->object_path); + priv->object_path = g_value_dup_string (value); + break; + case PROP_ID: + priv->id = g_value_get_uint (value); + break; + case PROP_MEDIA_TYPE: + priv->media_type = g_value_get_uint (value); + break; + case PROP_STATE: + priv->state = g_value_get_uint (value); + break; + case PROP_DIRECTION: + priv->direction = g_value_get_uint (value); + break; + case PROP_PENDING_SEND_FLAGS: + priv->pending_send_flags = g_value_get_uint (value); + break; + case PROP_HOLD_STATE: + priv->hold_state = g_value_get_boolean (value); + break; + case PROP_CREATED_LOCALLY: + priv->created_locally = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void rakia_media_stream_dispose (GObject *object); +static void rakia_media_stream_finalize (GObject *object); + +static void +rakia_media_stream_class_init (RakiaMediaStreamClass *klass) +{ + static TpDBusPropertiesMixinPropImpl stream_handler_props[] = { + { "CreatedLocally", "created-locally", NULL }, + { "NATTraversal", "nat-traversal", NULL }, + { "STUNServers", "stun-servers", NULL }, + { "RelayInfo", "relay-info", NULL }, + { NULL } + }; + + static TpDBusPropertiesMixinIfaceImpl prop_interfaces[] = { + { TP_IFACE_MEDIA_STREAM_HANDLER, + tp_dbus_properties_mixin_getter_gobject_properties, + NULL, + stream_handler_props, + }, + { NULL } + }; + + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GType stream_type = G_OBJECT_CLASS_TYPE (klass); + GParamSpec *param_spec; + + g_type_class_add_private (klass, sizeof (RakiaMediaStreamPrivate)); + + object_class->constructed = rakia_media_stream_constructed; + + object_class->get_property = rakia_media_stream_get_property; + object_class->set_property = rakia_media_stream_set_property; + + object_class->dispose = rakia_media_stream_dispose; + object_class->finalize = rakia_media_stream_finalize; + + param_spec = g_param_spec_object ("dbus-daemon", "TpDBusDaemon", + "Connection to D-Bus.", TP_TYPE_DBUS_DAEMON, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_DBUS_DAEMON, param_spec); + + param_spec = g_param_spec_object ("media-session", "RakiaMediaSession object", + "SIP media session object that owns this media stream object.", + TPSIP_TYPE_MEDIA_SESSION, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_MEDIA_SESSION, param_spec); + + param_spec = g_param_spec_string ("object-path", "D-Bus object path", + "The D-Bus object path used for this object on the bus.", + NULL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_OBJECT_PATH, param_spec); + + param_spec = g_param_spec_uint ("id", "Stream ID", + "A stream number for the stream used in the D-Bus API.", + 0, G_MAXUINT, + 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_ID, param_spec); + + param_spec = g_param_spec_uint ("media-type", "Stream media type", + "A constant indicating which media type the stream carries.", + TP_MEDIA_STREAM_TYPE_AUDIO, TP_MEDIA_STREAM_TYPE_VIDEO, + TP_MEDIA_STREAM_TYPE_AUDIO, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_MEDIA_TYPE, param_spec); + + param_spec = g_param_spec_uint ("state", "Connection state", + "Connection state of the media stream", + TP_MEDIA_STREAM_STATE_DISCONNECTED, TP_MEDIA_STREAM_STATE_CONNECTED, + TP_MEDIA_STREAM_STATE_DISCONNECTED, + G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_STATE, param_spec); + + /* We don't change the following two as individual properties + * after construction, use rakia_media_stream_set_direction() */ + + param_spec = g_param_spec_uint ("direction", "Stream direction", + "A value indicating the current direction of the stream", + TP_MEDIA_STREAM_DIRECTION_NONE, TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL, + TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_DIRECTION, param_spec); + + param_spec = g_param_spec_uint ("pending-send-flags", "Pending send flags", + "Flags indicating the current pending send state of the stream", + 0, + TP_MEDIA_STREAM_PENDING_LOCAL_SEND | TP_MEDIA_STREAM_PENDING_REMOTE_SEND, + 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, + PROP_PENDING_SEND_FLAGS, + param_spec); + + param_spec = g_param_spec_boolean ("hold-state", "Hold state", + "Hold state of the media stream as reported by the stream engine", + FALSE, + G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, + PROP_HOLD_STATE, + param_spec); + + param_spec = g_param_spec_boolean ("created-locally", "Created locally?", + "True if this stream was created by the local user", FALSE, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_CREATED_LOCALLY, + param_spec); + + param_spec = g_param_spec_string ("nat-traversal", "NAT traversal", + "NAT traversal mechanism for this stream", NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_NAT_TRAVERSAL, + param_spec); + + param_spec = g_param_spec_boxed ("stun-servers", "STUN servers", + "Array of IP address-port pairs for available STUN servers", + TP_ARRAY_TYPE_SOCKET_ADDRESS_IP_LIST, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_STUN_SERVERS, param_spec); + + param_spec = g_param_spec_boxed ("relay-info", "Relay info", + "Array of mappings containing relay server information", + TP_ARRAY_TYPE_STRING_VARIANT_MAP_LIST, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_RELAY_INFO, param_spec); + + rakia_media_stream_relay_info_empty = g_ptr_array_new (); + + /* signals not exported by DBus interface */ + signals[SIG_READY] = + g_signal_new ("ready", + stream_type, + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[SIG_SUPPORTED_CODECS] = + g_signal_new ("supported-codecs", + stream_type, + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + + signals[SIG_STATE_CHANGED] = + g_signal_new ("state-changed", + stream_type, + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__UINT, + G_TYPE_NONE, 1, G_TYPE_UINT); + + signals[SIG_DIRECTION_CHANGED] = + g_signal_new ("direction-changed", + stream_type, + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + 0, + NULL, NULL, + _rakia_marshal_VOID__UINT_UINT, + G_TYPE_NONE, 2, G_TYPE_UINT, G_TYPE_UINT); + + signals[SIG_LOCAL_MEDIA_UPDATED] = + g_signal_new ("local-media-updated", + stream_type, + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[SIG_UNHOLD_FAILURE] = + g_signal_new ("unhold-failure", + stream_type, + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + klass->dbus_props_class.interfaces = prop_interfaces; + tp_dbus_properties_mixin_class_init (object_class, + G_STRUCT_OFFSET (RakiaMediaStreamClass, dbus_props_class)); +} + +void +rakia_media_stream_dispose (GObject *object) +{ + RakiaMediaStream *self = TPSIP_MEDIA_STREAM (object); + RakiaMediaStreamPrivate *priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (self); + + if (priv->dispose_has_run) + return; + + priv->dispose_has_run = TRUE; + + tp_clear_object (&priv->dbus_daemon); + + if (G_OBJECT_CLASS (rakia_media_stream_parent_class)->dispose) + G_OBJECT_CLASS (rakia_media_stream_parent_class)->dispose (object); + + DEBUG ("exit"); +} + +void +rakia_media_stream_finalize (GObject *object) +{ + RakiaMediaStream *self = TPSIP_MEDIA_STREAM (object); + RakiaMediaStreamPrivate *priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (self); + + /* free any data held directly by the object here */ + g_free (priv->object_path); + g_free (priv->stream_sdp); + + g_value_unset (&priv->native_codecs); + g_value_unset (&priv->native_candidates); + + g_free (priv->native_candidate_id); + g_free (priv->remote_candidate_id); + + G_OBJECT_CLASS (rakia_media_stream_parent_class)->finalize (object); + + DEBUG ("exit"); +} + +/*********************************************************************** + * Set: Media.StreamHandler interface implementation (same for 0.12/0.13???) + ***********************************************************************/ + +/** + * rakia_media_stream_codec_choice + * + * Implements DBus method CodecChoice + * on interface org.freedesktop.Telepathy.Media.StreamHandler + */ +static void +rakia_media_stream_codec_choice (TpSvcMediaStreamHandler *iface, + guint codec_id, + DBusGMethodInvocation *context) +{ + /* Inform the connection manager of the current codec choice. + * -> note: not implemented by tp-gabble either (2006/May) */ + + DEBUG ("not implemented (ignoring)"); + + tp_svc_media_stream_handler_return_from_codec_choice (context); +} + +/** + * rakia_media_stream_error + * + * Implements DBus method Error + * on interface org.freedesktop.Telepathy.Media.StreamHandler + */ +static void +rakia_media_stream_error (TpSvcMediaStreamHandler *iface, + guint errno, + const gchar *message, + DBusGMethodInvocation *context) +{ + DEBUG("StreamHandler.Error called: %u %s", errno, message); + + rakia_media_stream_close (TPSIP_MEDIA_STREAM (iface)); + + tp_svc_media_stream_handler_return_from_error (context); +} + +/** + * rakia_media_stream_native_candidates_prepared + * + * Implements DBus method NativeCandidatesPrepared + * on interface org.freedesktop.Telepathy.Media.StreamHandler + */ +static void +rakia_media_stream_native_candidates_prepared (TpSvcMediaStreamHandler *iface, + DBusGMethodInvocation *context) +{ + /* purpose: "Informs the connection manager that all possible native candisates + * have been discovered for the moment." + */ + + RakiaMediaStream *obj = TPSIP_MEDIA_STREAM (iface); + RakiaMediaStreamPrivate *priv; + + priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (obj); + + DEBUG("enter"); + + priv->native_cands_prepared = TRUE; + + if (priv->native_codecs_prepared) + priv_generate_sdp (obj); + + push_active_candidate_pair (obj); + + tp_svc_media_stream_handler_return_from_native_candidates_prepared (context); +} + + +/** + * rakia_media_stream_new_active_candidate_pair + * + * Implements DBus method NewActiveCandidatePair + * on interface org.freedesktop.Telepathy.Media.StreamHandler + */ +static void +rakia_media_stream_new_active_candidate_pair (TpSvcMediaStreamHandler *iface, + const gchar *native_candidate_id, + const gchar *remote_candidate_id, + DBusGMethodInvocation *context) +{ + RakiaMediaStream *obj = TPSIP_MEDIA_STREAM (iface); + RakiaMediaStreamPrivate *priv; + + priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (obj); + + DEBUG("stream engine reported new active candidate pair %s-%s", + native_candidate_id, remote_candidate_id); + + if (priv->remote_candidate_id == NULL + || strcmp (priv->remote_candidate_id, remote_candidate_id)) + { + GError *err; + err = g_error_new (TP_ERRORS, + TP_ERROR_INVALID_ARGUMENT, + "Remote candidate ID does not match the locally " + "stored data"); + dbus_g_method_return_error (context, err); + g_error_free (err); + return; + } + + tp_svc_media_stream_handler_return_from_new_active_candidate_pair (context); +} + + +/** + * rakia_media_stream_new_native_candidate + * + * Implements DBus method NewNativeCandidate + * on interface org.freedesktop.Telepathy.Media.StreamHandler + */ +static void +rakia_media_stream_new_native_candidate (TpSvcMediaStreamHandler *iface, + const gchar *candidate_id, + const GPtrArray *transports, + DBusGMethodInvocation *context) +{ + RakiaMediaStream *obj = TPSIP_MEDIA_STREAM (iface); + RakiaMediaStreamPrivate *priv; + GPtrArray *candidates; + GValue candidate = { 0, }; + GValue transport = { 0, }; + gint tr_goodness; + + priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (obj); + + if (priv->stream_sdp != NULL) + { + MESSAGE ("Stream %u: SDP already generated, ignoring native candidate '%s'", priv->id, candidate_id); + tp_svc_media_stream_handler_return_from_new_native_candidate (context); + return; + } + + g_return_if_fail (transports->len >= 1); + + /* Rate the preferability of the address */ + g_value_init (&transport, TP_STRUCT_TYPE_MEDIA_STREAM_HANDLER_TRANSPORT); + g_value_set_static_boxed (&transport, g_ptr_array_index (transports, 0)); + tr_goodness = rakia_media_session_rate_native_transport (priv->session, + &transport); + + candidates = g_value_get_boxed (&priv->native_candidates); + + if (tr_goodness > 0) + { + DEBUG("native candidate '%s' is rated as preferable", candidate_id); + g_free (priv->native_candidate_id); + priv->native_candidate_id = g_strdup (candidate_id); + + /* Drop the candidates received previously */ + g_value_reset (&priv->native_candidates); + candidates = dbus_g_type_specialized_construct ( + TP_ARRAY_TYPE_MEDIA_STREAM_HANDLER_CANDIDATE_LIST); + g_value_take_boxed (&priv->native_candidates, candidates); + } + + g_value_init (&candidate, TP_STRUCT_TYPE_MEDIA_STREAM_HANDLER_CANDIDATE); + g_value_take_boxed (&candidate, + dbus_g_type_specialized_construct (TP_STRUCT_TYPE_MEDIA_STREAM_HANDLER_CANDIDATE)); + + dbus_g_type_struct_set (&candidate, + 0, candidate_id, + 1, transports, + G_MAXUINT); + + g_ptr_array_add (candidates, g_value_get_boxed (&candidate)); + + SESSION_DEBUG(priv->session, "put native candidate '%s' from stream-engine into cache", candidate_id); + + tp_svc_media_stream_handler_return_from_new_native_candidate (context); +} + +static void +priv_set_local_codecs (RakiaMediaStream *self, + const GPtrArray *codecs) +{ + RakiaMediaStreamPrivate *priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (self); + GValue val = { 0, }; + + SESSION_DEBUG(priv->session, "putting list of %d locally supported " + "codecs from stream-engine into cache", codecs->len); + g_value_init (&val, TP_ARRAY_TYPE_MEDIA_STREAM_HANDLER_CODEC_LIST); + g_value_set_static_boxed (&val, codecs); + g_value_copy (&val, &priv->native_codecs); + + priv->native_codecs_prepared = TRUE; + if (priv->native_cands_prepared) + priv_generate_sdp (self); +} + +static void +rakia_media_stream_codecs_updated (TpSvcMediaStreamHandler *iface, + const GPtrArray *codecs, + DBusGMethodInvocation *context) +{ + RakiaMediaStream *self = TPSIP_MEDIA_STREAM (iface); + RakiaMediaStreamPrivate *priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (self); + + if (!priv->native_codecs_prepared) + { + GError e = { TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "CodecsUpdated may not be called before codecs have been provided " + "with SetLocalCodecs or Ready" }; + + SESSION_DEBUG(priv->session, + "CodecsUpdated called before SetLocalCodecs or Ready"); + + dbus_g_method_return_error (context, &e); + } + else + { + GValue val = { 0, }; + + SESSION_DEBUG(priv->session, "putting list of %d locally supported " + "codecs from CodecsUpdated into cache", codecs->len); + g_value_init (&val, TP_ARRAY_TYPE_MEDIA_STREAM_HANDLER_CODEC_LIST); + g_value_set_static_boxed (&val, codecs); + g_value_copy (&val, &priv->native_codecs); + + /* This doesn't use priv_generate_sdp because it short-circuits if + * priv->stream_sdp is already set. We want to update it. + */ + if (priv->native_cands_prepared) + priv_update_local_sdp (self); + + tp_svc_media_stream_handler_return_from_codecs_updated (context); + } +} + +/** + * rakia_media_stream_ready + * + * Implements DBus method Ready + * on interface org.freedesktop.Telepathy.Media.StreamHandler + */ +static void +rakia_media_stream_ready (TpSvcMediaStreamHandler *iface, + const GPtrArray *codecs, + DBusGMethodInvocation *context) +{ + /* purpose: "Inform the connection manager that a client is ready to handle + * this StreamHandler. Also provide it with info about all supported + * codecs." + * + * - note, with SIP we don't send the invite just yet (we need + * candidates first + */ + + RakiaMediaStream *obj = TPSIP_MEDIA_STREAM (iface); + RakiaMediaStreamPrivate *priv; + + DEBUG ("enter"); + + priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (obj); + + if (priv->ready_received) + { + MESSAGE ("Ready called more than once"); + tp_svc_media_stream_handler_return_from_ready (context); + return; + } + + priv->ready_received = TRUE; + + if (codecs->len != 0) + priv_set_local_codecs (obj, codecs); + + /* Push the initial sending/playing state */ + tp_svc_media_stream_handler_emit_set_stream_playing ( + iface, priv->playing); + tp_svc_media_stream_handler_emit_set_stream_sending ( + iface, priv->sending); + + priv->native_codecs_prepared = TRUE; + if (priv->native_cands_prepared) + priv_generate_sdp (obj); + + if (priv->push_remote_cands_pending) + { + priv->push_remote_cands_pending = FALSE; + push_remote_candidates (obj); + } + if (priv->push_remote_codecs_pending) + { + priv->push_remote_codecs_pending = FALSE; + push_remote_codecs (obj); + } + + /* note: for inbound sessions, emit active candidate pair once + remote info is set */ + push_active_candidate_pair (obj); + + tp_svc_media_stream_handler_return_from_ready (context); +} + +static void +rakia_media_stream_set_local_codecs (TpSvcMediaStreamHandler *iface, + const GPtrArray *codecs, + DBusGMethodInvocation *context) +{ + priv_set_local_codecs (TPSIP_MEDIA_STREAM (iface), codecs); + tp_svc_media_stream_handler_return_from_set_local_codecs (context); +} + +/** + * rakia_media_stream_stream_state + * + * Implements DBus method StreamState + * on interface org.freedesktop.Telepathy.Media.StreamHandler + */ +static void +rakia_media_stream_stream_state (TpSvcMediaStreamHandler *iface, + guint state, + DBusGMethodInvocation *context) +{ + /* purpose: "Informs the connection manager of the stream's current state + * State is as specified in *ChannelTypeStreamedMedia::GetStreams." + * + * - set the stream state for session + */ + + RakiaMediaStream *obj = TPSIP_MEDIA_STREAM (iface); + RakiaMediaStreamPrivate *priv; + priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (obj); + + if (priv->state != state) + { + DEBUG("changing stream state from %u to %u", priv->state, state); + priv->state = state; + g_signal_emit (obj, signals[SIG_STATE_CHANGED], 0, state); + } + + tp_svc_media_stream_handler_return_from_stream_state (context); +} + +/** + * rakia_media_stream_supported_codecs + * + * Implements DBus method SupportedCodecs + * on interface org.freedesktop.Telepathy.Media.StreamHandler + */ +static void +rakia_media_stream_supported_codecs (TpSvcMediaStreamHandler *iface, + const GPtrArray *codecs, + DBusGMethodInvocation *context) +{ + /* purpose: "Inform the connection manager of the supported codecs for this session. + * This is called after the connection manager has emitted SetRemoteCodecs + * to notify what codecs are supported by the peer, and will thus be an + * intersection of all locally supported codecs (passed to Ready) + * and those supported by the peer." + * + * - emit SupportedCodecs + */ + + RakiaMediaStream *self = TPSIP_MEDIA_STREAM (iface); + RakiaMediaStreamPrivate *priv; + priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (self); + + DEBUG("got codec intersection containing %u codecs from stream-engine", + codecs->len); + + /* Uncomment the line below if there's need to limit the local codec list + * with the intersection for later SDP negotiations. + * TODO: Make sure to update the SDP for the stream as well. */ + /* g_value_set_boxed (&priv->native_codecs, codecs); */ + + if (priv->codec_intersect_pending) + { + if (priv->push_remote_codecs_pending) + { + /* The remote codec list has been updated since the intersection + * has started, plunge into a new intersection immediately */ + priv->push_remote_codecs_pending = FALSE; + push_remote_codecs (self); + } + else + { + priv->codec_intersect_pending = FALSE; + g_signal_emit (self, signals[SIG_SUPPORTED_CODECS], 0, codecs->len); + } + } + else + WARNING("SupportedCodecs called when no intersection is ongoing"); + + tp_svc_media_stream_handler_return_from_supported_codecs (context); +} + +static void +rakia_media_stream_hold_state (TpSvcMediaStreamHandler *self, + gboolean held, + DBusGMethodInvocation *context) +{ + g_object_set (self, "hold-state", held, NULL); + tp_svc_media_stream_handler_return_from_hold_state (context); +} + +static void +rakia_media_stream_unhold_failure (TpSvcMediaStreamHandler *self, + DBusGMethodInvocation *context) +{ + /* Not doing anything to hold_state or requested_hold_state, + * because the session is going to put all streams on hold after getting + * the signal below */ + + g_signal_emit (self, signals[SIG_UNHOLD_FAILURE], 0); + tp_svc_media_stream_handler_return_from_unhold_failure (context); +} + +/*********************************************************************** + * Helper functions follow (not based on generated templates) + ***********************************************************************/ + +guint +rakia_media_stream_get_id (RakiaMediaStream *self) +{ + RakiaMediaStreamPrivate *priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (self); + return priv->id; +} + +guint +rakia_media_stream_get_media_type (RakiaMediaStream *self) +{ + RakiaMediaStreamPrivate *priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (self); + return priv->media_type; +} + +void +rakia_media_stream_close (RakiaMediaStream *self) +{ + tp_svc_media_stream_handler_emit_close (self); +} + +/** + * Described the local stream configuration in SDP (RFC2327), + * or NULL if stream not configured yet. + */ +const char *rakia_media_stream_local_sdp (RakiaMediaStream *obj) +{ + RakiaMediaStreamPrivate *priv; + priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (obj); + return priv->stream_sdp; +} + +TpMediaStreamDirection +rakia_media_stream_direction_from_remote_media (const sdp_media_t *media) +{ + sdp_mode_t mode = media->m_mode; + return ((mode & sdp_recvonly)? TP_MEDIA_STREAM_DIRECTION_SEND : 0) + | ((mode & sdp_sendonly)? TP_MEDIA_STREAM_DIRECTION_RECEIVE : 0); +} + +static gboolean +rakia_sdp_codecs_differ (const sdp_rtpmap_t *m1, const sdp_rtpmap_t *m2) +{ + while (m1 != NULL && m2 != NULL) + { + if (sdp_rtpmap_cmp (m1, m2) != 0) + return TRUE; + m1 = m1->rm_next; + m2 = m2->rm_next; + } + return m1 != NULL || m2 != NULL; +} + +/* + * Returns stream direction as requested by the latest local or remote + * direction change. + */ +static TpMediaStreamDirection +priv_get_requested_direction (RakiaMediaStreamPrivate *priv) +{ + TpMediaStreamDirection direction; + + direction = priv->direction; + if ((priv->pending_send_flags & TP_MEDIA_STREAM_PENDING_LOCAL_SEND) != 0) + direction |= TP_MEDIA_STREAM_DIRECTION_SEND; + return direction; +} + +/** + * Sets the remote candidates and codecs for this stream, as + * received via signaling. + * + * Parses the SDP information, updates TP remote candidates and + * codecs if the client is ready. + * + * Note that the pointer to the media description structure is saved, + * implying that the structure shall not go away for the lifetime of + * the stream, preferably kept in the memory home attached to + * the session object. + * + * @return TRUE if the remote information has been accepted, + * FALSE if the update is not acceptable. + */ +gboolean +rakia_media_stream_set_remote_media (RakiaMediaStream *stream, + const sdp_media_t *new_media, + guint direction_up_mask, + guint pending_send_mask) +{ + RakiaMediaStreamPrivate *priv; + sdp_connection_t *sdp_conn; + const sdp_media_t *old_media; + gboolean transport_changed = TRUE; + gboolean codecs_changed = TRUE; + guint old_direction; + guint new_direction; + + DEBUG ("enter"); + + priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (stream); + + /* Do sanity checks */ + + g_return_val_if_fail (new_media != NULL, FALSE); + + if (new_media->m_rejected || new_media->m_port == 0) + { + DEBUG("the stream is rejected remotely"); + return FALSE; + } + + if (new_media->m_proto != sdp_proto_rtp) + { + WARNING ("Stream %u: the remote protocol is not RTP/AVP", priv->id); + return FALSE; + } + + sdp_conn = sdp_media_connections (new_media); + if (sdp_conn == NULL) + { + WARNING ("Stream %u: no valid remote connections", priv->id); + return FALSE; + } + + if (new_media->m_rtpmaps == NULL) + { + WARNING ("Stream %u: no remote codecs", priv->id); + return FALSE; + } + + /* Note: always update the pointer to the current media structure + * because of memory management done in the session object */ + old_media = priv->remote_media; + priv->remote_media = new_media; + + /* Check if there was any media update at all */ + + if (sdp_media_cmp (old_media, new_media) == 0) + { + DEBUG("no media changes detected for the stream"); + return TRUE; + } + + old_direction = priv_get_requested_direction (priv); + new_direction = rakia_media_stream_direction_from_remote_media (new_media); + + /* Make sure the peer can only enable sending or receiving direction + * if it's allowed to */ + new_direction &= old_direction | direction_up_mask; + + if (old_media != NULL) + { + /* Check if the transport candidate needs to be changed */ + if (!sdp_connection_cmp (sdp_media_connections (old_media), sdp_conn)) + transport_changed = FALSE; + + /* Check if the codec list needs to be updated */ + codecs_changed = rakia_sdp_codecs_differ (old_media->m_rtpmaps, + new_media->m_rtpmaps); + + /* Disable sending at this point if it will be disabled + * accordingly to the new direction */ + priv_update_sending (stream, + priv->direction & new_direction); + } + + /* First add the new candidate, then update the codec set. + * The offerer isn't supposed to send us anything from the new transport + * until we accept; if it's the answer, both orderings have problems. */ + + if (transport_changed) + { + /* Make sure we stop sending before we use the new set of codecs + * intended for the new connection */ + if (codecs_changed) + rakia_media_stream_set_sending (stream, FALSE); + + push_remote_candidates (stream); + } + + if (codecs_changed) + { + if (!priv->codec_intersect_pending) + { + priv->codec_intersect_pending = TRUE; + push_remote_codecs (stream); + } + else + { + priv->push_remote_codecs_pending = TRUE; + } + } + + /* TODO: this will go to session change commit code */ + + /* note: for outbound sessions (for which remote cands become + * available at a later stage), emit active candidate pair + * (and playing status?) once remote info set */ + push_active_candidate_pair (stream); + + /* Set the final direction and update pending send flags */ + rakia_media_stream_set_direction (stream, + new_direction, + pending_send_mask); + + return TRUE; +} + +/** + * Converts a sofia-sip media type enum to Telepathy media type. + * See <sofia-sip/sdp.h> and <telepathy-constants.h>. + * + * @return G_MAXUINT if the media type cannot be mapped + */ +guint +rakia_tp_media_type (sdp_media_e sip_mtype) +{ + switch (sip_mtype) + { + case sdp_media_audio: return TP_MEDIA_STREAM_TYPE_AUDIO; + case sdp_media_video: return TP_MEDIA_STREAM_TYPE_VIDEO; + default: return G_MAXUINT; + } +} + +/** + * Sets the media state to playing or non-playing. When not playing, + * received RTP packets may not be played locally. + */ +void rakia_media_stream_set_playing (RakiaMediaStream *stream, gboolean playing) +{ + RakiaMediaStreamPrivate *priv; + priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (stream); + + if (same_boolean (priv->playing, playing)) + return; + + DEBUG("set playing to %s", playing? "TRUE" : "FALSE"); + + priv->playing = playing; + + if (priv->ready_received) + tp_svc_media_stream_handler_emit_set_stream_playing ( + (TpSvcMediaStreamHandler *)stream, playing); +} + +/** + * Sets the media state to sending or non-sending. When not sending, + * captured media are not sent over the network. + */ +void +rakia_media_stream_set_sending (RakiaMediaStream *stream, gboolean sending) +{ + RakiaMediaStreamPrivate *priv; + priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (stream); + + if (same_boolean(priv->sending, sending)) + return; + + DEBUG("set sending to %s", sending? "TRUE" : "FALSE"); + + priv->sending = sending; + + if (priv->ready_received) + tp_svc_media_stream_handler_emit_set_stream_sending ( + (TpSvcMediaStreamHandler *)stream, sending); +} + +static void +priv_update_sending (RakiaMediaStream *stream, + TpMediaStreamDirection direction) +{ + RakiaMediaStreamPrivate *priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (stream); + gboolean sending = TRUE; + + /* XXX: the pending send flag check is probably an overkill + * considering that effective sending direction and pending send should be + * mutually exclusive */ + if ((direction & TP_MEDIA_STREAM_DIRECTION_SEND) == 0 + || priv->pending_remote_receive + || (priv->pending_send_flags & TP_MEDIA_STREAM_PENDING_LOCAL_SEND) != 0 + || !rakia_media_session_is_accepted (priv->session)) + { + sending = FALSE; + } + + rakia_media_stream_set_sending (stream, sending); +} + +void +rakia_media_stream_set_direction (RakiaMediaStream *stream, + TpMediaStreamDirection direction, + guint pending_send_mask) +{ + RakiaMediaStreamPrivate *priv; + guint pending_send_flags; + TpMediaStreamDirection old_sdp_direction; + + priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (stream); + pending_send_flags = priv->pending_send_flags & pending_send_mask; + + if ((direction & ~priv->direction & TP_MEDIA_STREAM_DIRECTION_SEND) != 0) + { + /* We are requested to start sending, but... */ + if ((pending_send_mask + & TP_MEDIA_STREAM_PENDING_LOCAL_SEND) != 0) + { + /* ... but we need to confirm this with the client. + * Clear the sending bit and set the pending send flag. */ + direction &= ~(guint)TP_MEDIA_STREAM_DIRECTION_SEND; + pending_send_flags |= TP_MEDIA_STREAM_PENDING_LOCAL_SEND; + } + if ((pending_send_mask + & TP_MEDIA_STREAM_PENDING_REMOTE_SEND) != 0 + && (priv->pending_send_flags + & TP_MEDIA_STREAM_PENDING_LOCAL_SEND) == 0) + { + g_assert ((priv_get_requested_direction (priv) & TP_MEDIA_STREAM_DIRECTION_SEND) == 0); + + /* ... but the caller wants to agree with the remote + * end first. Block the stream handler from sending for now. */ + priv->pending_remote_receive = TRUE; + } + } + if ((direction & ~priv->direction & TP_MEDIA_STREAM_DIRECTION_RECEIVE) != 0 + && (pending_send_mask + & TP_MEDIA_STREAM_PENDING_REMOTE_SEND) != 0) + { + /* We're requested to start receiving, but the remote end did not + * confirm if it will send. Set the pending send flag. */ + pending_send_flags |= TP_MEDIA_STREAM_PENDING_REMOTE_SEND; + } + + if (priv->direction == direction + && priv->pending_send_flags == pending_send_flags) + return; + + old_sdp_direction = priv_get_requested_direction (priv); + + priv->direction = direction; + priv->pending_send_flags = pending_send_flags; + + DEBUG("set direction %u, pending send flags %u", priv->direction, priv->pending_send_flags); + + g_signal_emit (stream, signals[SIG_DIRECTION_CHANGED], 0, + priv->direction, priv->pending_send_flags); + + if (priv->remote_media != NULL) + priv_update_sending (stream, priv->direction); + + if (priv->native_cands_prepared + && priv->native_codecs_prepared + && priv_get_requested_direction (priv) + != old_sdp_direction) + priv_update_local_sdp (stream); +} + +/* + * Clears the pending send flag(s) present in @pending_send_mask. + * If #TP_MEDIA_STREAM_PENDING_LOCAL_SEND is thus cleared, + * enable the sending bit in the stream direction. + * If @pending_send_mask has #TP_MEDIA_STREAM_PENDING_REMOTE_SEND flag set, + * also start sending if agreed by the stream direction. + */ +void +rakia_media_stream_apply_pending_direction (RakiaMediaStream *stream, + guint pending_send_mask) +{ + RakiaMediaStreamPrivate *priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (stream); + guint flags; + + + /* Don't apply pending send for new streams that haven't been negotiated */ + if (priv->remote_media == NULL) + return; + + /* Remember the flags that got changes and then clear the set */ + flags = (priv->pending_send_flags & pending_send_mask); + priv->pending_send_flags &= ~pending_send_mask; + + if (flags != 0) + { + if ((flags & TP_MEDIA_STREAM_PENDING_LOCAL_SEND) != 0) + priv->direction |= TP_MEDIA_STREAM_DIRECTION_SEND; + + DEBUG("set direction %u, pending send flags %u", priv->direction, priv->pending_send_flags); + + g_signal_emit (stream, signals[SIG_DIRECTION_CHANGED], 0, + priv->direction, priv->pending_send_flags); + } + + if ((pending_send_mask & TP_MEDIA_STREAM_PENDING_REMOTE_SEND) != 0) + { + priv->pending_remote_receive = FALSE; + DEBUG("remote end ready to receive"); + } + + /* Always check to enable sending because the session could become accepted */ + priv_update_sending (stream, priv->direction); +} + +TpMediaStreamDirection +rakia_media_stream_get_requested_direction (RakiaMediaStream *self) +{ + return priv_get_requested_direction (TPSIP_MEDIA_STREAM_GET_PRIVATE (self)); +} + +/** + * Returns true if the stream has a valid SDP description and + * connection has been established with the stream engine. + */ +gboolean rakia_media_stream_is_local_ready (RakiaMediaStream *self) +{ + RakiaMediaStreamPrivate *priv; + priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (self); + g_assert (priv->stream_sdp == NULL || priv->ready_received); + return (priv->stream_sdp != NULL); +} + +gboolean +rakia_media_stream_is_codec_intersect_pending (RakiaMediaStream *self) +{ + RakiaMediaStreamPrivate *priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (self); + return priv->codec_intersect_pending; +} + +void +rakia_media_stream_start_telephony_event (RakiaMediaStream *self, guchar event) +{ + tp_svc_media_stream_handler_emit_start_telephony_event ( + (TpSvcMediaStreamHandler *)self, event); +} + +void +rakia_media_stream_stop_telephony_event (RakiaMediaStream *self) +{ + tp_svc_media_stream_handler_emit_stop_telephony_event ( + (TpSvcMediaStreamHandler *)self); +} + +gboolean +rakia_media_stream_request_hold_state (RakiaMediaStream *self, gboolean hold) +{ + RakiaMediaStreamPrivate *priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (self); + + if ((!priv->requested_hold_state) != (!hold)) + { + priv->requested_hold_state = hold; + tp_svc_media_stream_handler_emit_set_stream_held (self, hold); + return TRUE; + } + return FALSE; +} + +static void +priv_generate_sdp (RakiaMediaStream *self) +{ + RakiaMediaStreamPrivate *priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (self); + + if (priv->stream_sdp != NULL) + return; + + priv_update_local_sdp (self); + + g_assert (priv->stream_sdp != NULL); + + g_signal_emit (self, signals[SIG_READY], 0); +} + +/** + * Notify StreamEngine of remote codecs. + * + * @pre Ready signal must be receiveid (priv->ready_received) + */ +static void push_remote_codecs (RakiaMediaStream *stream) +{ + RakiaMediaStreamPrivate *priv; + GPtrArray *codecs; + GHashTable *opt_params; + GType codecs_type; + GType codec_type; + const sdp_media_t *sdpmedia; + const sdp_rtpmap_t *rtpmap; + gchar *ptime = NULL; + gchar *max_ptime = NULL; + + DEBUG ("enter"); + + priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (stream); + + sdpmedia = priv->remote_media; + if (sdpmedia == NULL) + { + DEBUG("remote media description is not received yet"); + return; + } + + if (!priv->ready_received) + { + DEBUG("the stream engine is not ready, SetRemoteCodecs is pending"); + priv->push_remote_codecs_pending = TRUE; + return; + } + + ptime = rakia_sdp_get_string_attribute (sdpmedia->m_attributes, "ptime"); + if (ptime == NULL) + { + g_object_get (priv->session, + "remote-ptime", &ptime, + NULL); + } + max_ptime = rakia_sdp_get_string_attribute (sdpmedia->m_attributes, "maxptime"); + if (max_ptime == NULL) + { + g_object_get (priv->session, + "remote-max-ptime", &max_ptime, + NULL); + } + + codec_type = TP_STRUCT_TYPE_MEDIA_STREAM_HANDLER_CODEC; + codecs_type = TP_ARRAY_TYPE_MEDIA_STREAM_HANDLER_CODEC_LIST; + + codecs = dbus_g_type_specialized_construct (codecs_type); + opt_params = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + g_free); + + rtpmap = sdpmedia->m_rtpmaps; + while (rtpmap) + { + GValue codec = { 0, }; + + g_value_init (&codec, codec_type); + g_value_take_boxed (&codec, + dbus_g_type_specialized_construct (codec_type)); + + if (ptime != NULL) + g_hash_table_insert (opt_params, + g_strdup("ptime"), g_strdup (ptime)); + if (max_ptime != NULL) + g_hash_table_insert (opt_params, + g_strdup("maxptime"), g_strdup (max_ptime)); + + rakia_codec_param_parse (priv->media_type, rtpmap->rm_encoding, + rtpmap->rm_fmtp, opt_params); + + /* RFC2327: see "m=" line definition + * - note, 'encoding_params' is assumed to be channel + * count (i.e. channels in farsight) */ + + dbus_g_type_struct_set (&codec, + /* payload type: */ + 0, rtpmap->rm_pt, + /* encoding name: */ + 1, rtpmap->rm_encoding, + /* media type */ + 2, (guint)priv->media_type, + /* clock-rate */ + 3, rtpmap->rm_rate, + /* number of supported channels: */ + 4, rtpmap->rm_params ? atoi(rtpmap->rm_params) : 0, + /* optional params: */ + 5, opt_params, + G_MAXUINT); + + g_hash_table_remove_all (opt_params); + + g_ptr_array_add (codecs, g_value_get_boxed (&codec)); + + rtpmap = rtpmap->rm_next; + } + + g_hash_table_destroy (opt_params); + g_free (ptime); + g_free (max_ptime); + + SESSION_DEBUG(priv->session, "passing %d remote codecs to stream engine", + codecs->len); + + tp_svc_media_stream_handler_emit_set_remote_codecs ( + (TpSvcMediaStreamHandler *)stream, codecs); + + g_boxed_free (codecs_type, codecs); +} + +static void push_remote_candidates (RakiaMediaStream *stream) +{ + RakiaMediaStreamPrivate *priv; + GValue candidate = { 0 }; + GValue transport = { 0 }; + GValue transport_rtcp = { 0 }; + GPtrArray *candidates; + GPtrArray *transports; + GType candidate_type; + GType candidates_type; + GType transport_type; + GType transports_type; + const sdp_media_t *media; + const sdp_connection_t *sdp_conn; + gchar *candidate_id; + guint port; + + DEBUG("enter"); + + priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (stream); + + media = priv->remote_media; + if (media == NULL) + { + DEBUG("remote media description is not received yet"); + return; + } + + if (!priv->ready_received) + { + DEBUG("the stream engine is not ready, SetRemoteCandidateList is pending"); + priv->push_remote_cands_pending = TRUE; + return; + } + + /* use the address from SDP c-line as the only remote candidate */ + + sdp_conn = sdp_media_connections (media); + g_return_if_fail (sdp_conn != NULL); + + port = (guint) media->m_port; + + transports_type = TP_ARRAY_TYPE_MEDIA_STREAM_HANDLER_TRANSPORT_LIST; + transports = dbus_g_type_specialized_construct (transports_type); + + transport_type = TP_STRUCT_TYPE_MEDIA_STREAM_HANDLER_TRANSPORT; + g_value_init (&transport, transport_type); + g_value_take_boxed (&transport, + dbus_g_type_specialized_construct (transport_type)); + dbus_g_type_struct_set (&transport, + 0, 1, /* component number */ + 1, sdp_conn->c_address, + 2, port, + 3, TP_MEDIA_STREAM_BASE_PROTO_UDP, + 4, "RTP", + 5, "AVP", + /* 6, 0.0f, */ + 7, TP_MEDIA_STREAM_TRANSPORT_TYPE_LOCAL, + /* 8, "", */ + /* 9, "", */ + G_MAXUINT); + + DEBUG("remote RTP address=<%s>, port=<%u>", sdp_conn->c_address, port); + g_ptr_array_add (transports, g_value_get_boxed (&transport)); + + if (!rakia_sdp_rtcp_bandwidth_throttled (media->m_bandwidths)) + { + gboolean session_rtcp_enabled = TRUE; + g_object_get (priv->session, + "rtcp-enabled", &session_rtcp_enabled, + NULL); + if (session_rtcp_enabled) + { + const sdp_attribute_t *rtcp_attr; + const char *rtcp_address; + guint rtcp_port; + + /* Get the port and optional address for RTCP accordingly to RFC 3605 */ + rtcp_address = sdp_conn->c_address; + rtcp_attr = sdp_attribute_find (media->m_attributes, "rtcp"); + if (rtcp_attr == NULL || rtcp_attr->a_value == NULL) + { + rtcp_port = port + 1; + } + else + { + const char *rest; + rtcp_port = (guint) g_ascii_strtoull (rtcp_attr->a_value, + (gchar **) &rest, + 10); + if (rtcp_port != 0 + && (strncmp (rest, " IN IP4 ", 8) == 0 + || strncmp (rest, " IN IP6 ", 8) == 0)) + rtcp_address = rest + 8; + } + + g_value_init (&transport_rtcp, transport_type); + g_value_take_boxed (&transport_rtcp, + dbus_g_type_specialized_construct (transport_type)); + dbus_g_type_struct_set (&transport_rtcp, + 0, 2, /* component number */ + 1, rtcp_address, + 2, rtcp_port, + 3, TP_MEDIA_STREAM_BASE_PROTO_UDP, + 4, "RTCP", + 5, "AVP", + /* 6, 0.0f, */ + 7, TP_MEDIA_STREAM_TRANSPORT_TYPE_LOCAL, + /* 8, "", */ + /* 9, "", */ + G_MAXUINT); + + DEBUG("remote RTCP address=<%s>, port=<%u>", rtcp_address, rtcp_port); + g_ptr_array_add (transports, g_value_get_boxed (&transport_rtcp)); + } + } + + g_free (priv->remote_candidate_id); + candidate_id = g_strdup_printf ("L%u", ++priv->remote_candidate_counter); + priv->remote_candidate_id = candidate_id; + + candidate_type = TP_STRUCT_TYPE_MEDIA_STREAM_HANDLER_CANDIDATE; + g_value_init (&candidate, candidate_type); + g_value_take_boxed (&candidate, + dbus_g_type_specialized_construct (candidate_type)); + dbus_g_type_struct_set (&candidate, + 0, candidate_id, + 1, transports, + G_MAXUINT); + + candidates_type = TP_ARRAY_TYPE_MEDIA_STREAM_HANDLER_CANDIDATE_LIST; + candidates = dbus_g_type_specialized_construct (candidates_type); + g_ptr_array_add (candidates, g_value_get_boxed (&candidate)); + + DEBUG("emitting SetRemoteCandidateList with %s", candidate_id); + + tp_svc_media_stream_handler_emit_set_remote_candidate_list ( + (TpSvcMediaStreamHandler *)stream, candidates); + + g_boxed_free (candidates_type, candidates); + g_boxed_free (transports_type, transports); +} + +static void +push_active_candidate_pair (RakiaMediaStream *stream) +{ + RakiaMediaStreamPrivate *priv; + + DEBUG("enter"); + + priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (stream); + + if (priv->ready_received + && priv->native_candidate_id != NULL + && priv->remote_candidate_id != NULL) + { + DEBUG("emitting SetActiveCandidatePair for %s-%s", + priv->native_candidate_id, priv->remote_candidate_id); + tp_svc_media_stream_handler_emit_set_active_candidate_pair ( + stream, priv->native_candidate_id, priv->remote_candidate_id); + } +} + +static const char* priv_media_type_to_str(guint media_type) +{ +switch (media_type) + { + case TP_MEDIA_STREAM_TYPE_AUDIO: return "audio"; + case TP_MEDIA_STREAM_TYPE_VIDEO: return "video"; + default: g_assert_not_reached (); + ; + } +return "-"; +} + +static void +priv_append_rtpmaps (const GPtrArray *codecs, GString *mline, GString *alines) +{ + GValue codec = { 0, }; + gchar *co_name = NULL; + guint co_id; + guint co_type; + guint co_clockrate; + guint co_channels; + GHashTable *co_params = NULL; + guint i; + + g_value_init (&codec, TP_STRUCT_TYPE_MEDIA_STREAM_HANDLER_CODEC); + + for (i = 0; i < codecs->len; i++) + { + g_value_set_static_boxed (&codec, g_ptr_array_index (codecs, i)); + + dbus_g_type_struct_get (&codec, + 0, &co_id, + 1, &co_name, + 2, &co_type, + 3, &co_clockrate, + 4, &co_channels, + 5, &co_params, + G_MAXUINT); + + /* g_return_if_fail (co_type == priv->media_type); */ + + /* Add rtpmap entry to the a= lines */ + g_string_append_printf (alines, + "a=rtpmap:%u %s/%u", + co_id, + co_name, + co_clockrate); + if (co_channels > 1) + g_string_append_printf (alines, "/%u", co_channels); + g_string_append (alines, "\r\n"); + + /* Marshal parameters into the fmtp attribute */ + if (g_hash_table_size (co_params) != 0) + { + GString *fmtp_value; + g_string_append_printf (alines, "a=fmtp:%u ", co_id); + fmtp_value = g_string_new (NULL); + rakia_codec_param_format (co_type, co_name, + co_params, fmtp_value); + g_string_append (alines, fmtp_value->str); + g_string_free (fmtp_value, TRUE); + g_string_append (alines, "\r\n"); + } + + /* Add PT id to the m= line */ + g_string_append_printf (mline, " %u", co_id); + + g_free (co_name); + co_name = NULL; + g_hash_table_destroy (co_params); + co_params = NULL; + } +} + +/** +* Refreshes the local SDP based on Farsight stream, and current +* object, state. +*/ +static void +priv_update_local_sdp(RakiaMediaStream *stream) +{ + RakiaMediaStreamPrivate *priv; + GString *mline; + GString *alines; + gchar *cline; + GValue transport = { 0 }; + const GPtrArray *candidates; + gchar *tr_addr = NULL; + /* gchar *tr_user = NULL; */ + /* gchar *tr_pass = NULL; */ + gchar *tr_subtype = NULL; + gchar *tr_profile = NULL; + guint tr_port; + guint tr_component; + /* guint tr_type; */ + /* gdouble tr_pref; */ + guint rtcp_port = 0; + gchar *rtcp_address = NULL; + const gchar *dirline; + int i; + + priv = TPSIP_MEDIA_STREAM_GET_PRIVATE (stream); + + candidates = g_value_get_boxed (&priv->native_candidates); + + g_value_init (&transport, TP_STRUCT_TYPE_MEDIA_STREAM_HANDLER_TRANSPORT); + + /* Find the preferred candidate, if defined, + * else the last acceptable candidate */ + + for (i = candidates->len - 1; i >= 0; --i) + { + GValueArray *candidate; + const gchar *candidate_id; + const GPtrArray *ca_tports; + guint tr_proto = TP_MEDIA_STREAM_BASE_PROTO_UDP; + guint j; + + candidate = g_ptr_array_index (candidates, i); + candidate_id = + g_value_get_string (g_value_array_get_nth (candidate, 0)); + ca_tports = g_value_get_boxed (g_value_array_get_nth (candidate, 1)); + + if (ca_tports->len == 0) + { + WARNING ("candidate '%s' lists no transports, skipping", candidate_id); + continue; + } + + for (j = 0; j < ca_tports->len; j++) + { + g_value_set_static_boxed (&transport, + g_ptr_array_index (ca_tports, j)); + dbus_g_type_struct_get (&transport, + 0, &tr_component, + G_MAXUINT); + switch (tr_component) + { + case 1: /* RTP */ + dbus_g_type_struct_get (&transport, + 1, &tr_addr, + 2, &tr_port, + 3, &tr_proto, + 4, &tr_subtype, + 5, &tr_profile, + /* 6, &tr_pref, */ + /* 7, &tr_type, */ + /* 8, &tr_user, */ + /* 9, &tr_pass, */ + G_MAXUINT); + break; + case 2: /* RTCP */ + dbus_g_type_struct_get (&transport, + 1, &rtcp_address, + 2, &rtcp_port, + G_MAXUINT); + break; + } + } + + if (priv->native_candidate_id != NULL) + { + if (!strcmp (candidate_id, priv->native_candidate_id)) + break; + } + else if (tr_proto == TP_MEDIA_STREAM_BASE_PROTO_UDP) + { + g_free (priv->native_candidate_id); + priv->native_candidate_id = g_strdup (candidate_id); + break; + } + } + g_return_if_fail (i >= 0); + g_return_if_fail (tr_addr != NULL); + g_return_if_fail (tr_subtype != NULL); + g_return_if_fail (tr_profile != NULL); + + mline = g_string_new ("m="); + g_string_append_printf (mline, + "%s %u %s/%s", + priv_media_type_to_str (priv->media_type), + tr_port, + tr_subtype, + tr_profile); + + cline = g_strdup_printf ("c=IN %s %s\r\n", + (strchr (tr_addr, ':') == NULL)? "IP4" : "IP6", + tr_addr); + + switch (priv_get_requested_direction (priv)) + { + case TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL: + dirline = ""; + break; + case TP_MEDIA_STREAM_DIRECTION_SEND: + dirline = "a=sendonly\r\n"; + break; + case TP_MEDIA_STREAM_DIRECTION_RECEIVE: + dirline = "a=recvonly\r\n"; + break; + case TP_MEDIA_STREAM_DIRECTION_NONE: + dirline = "a=inactive\r\n"; + break; + default: + g_assert_not_reached(); + } + + alines = g_string_new (dirline); + + if (rtcp_address != NULL) + { + /* Add RTCP attribute as per RFC 3605 */ + if (strcmp (rtcp_address, tr_addr) != 0) + { + g_string_append_printf (alines, + "a=rtcp:%u IN %s %s\r\n", + rtcp_port, + (strchr (rtcp_address, ':') == NULL) + ? "IP4" : "IP6", + rtcp_address); + } + else if (rtcp_port != tr_port + 1) + { + g_string_append_printf (alines, + "a=rtcp:%u\r\n", + rtcp_port); + } + } + + priv_append_rtpmaps (g_value_get_boxed (&priv->native_codecs), + mline, alines); + + g_free(priv->stream_sdp); + priv->stream_sdp = g_strconcat(mline->str, "\r\n", + cline, + alines->str, + NULL); + + g_free (tr_addr); + g_free (tr_profile); + g_free (tr_subtype); + /* g_free (tr_user); */ + /* g_free (tr_pass); */ + g_free (rtcp_address); + + g_string_free (mline, TRUE); + g_free (cline); + g_string_free (alines, TRUE); + + g_signal_emit (stream, signals[SIG_LOCAL_MEDIA_UPDATED], 0); +} + +static void +stream_handler_iface_init (gpointer g_iface, gpointer iface_data) +{ + TpSvcMediaStreamHandlerClass *klass = (TpSvcMediaStreamHandlerClass *)g_iface; + +#define IMPLEMENT(x) tp_svc_media_stream_handler_implement_##x (\ + klass, (tp_svc_media_stream_handler_##x##_impl) rakia_media_stream_##x) + IMPLEMENT(codec_choice); + IMPLEMENT(error); + IMPLEMENT(native_candidates_prepared); + IMPLEMENT(new_active_candidate_pair); + IMPLEMENT(new_native_candidate); + IMPLEMENT(ready); + IMPLEMENT(set_local_codecs); + IMPLEMENT(codecs_updated); + IMPLEMENT(stream_state); + IMPLEMENT(supported_codecs); + IMPLEMENT(hold_state); + IMPLEMENT(unhold_failure); +#undef IMPLEMENT +} diff --git a/rakia/media-stream.h b/rakia/media-stream.h new file mode 100644 index 0000000..0e09a42 --- /dev/null +++ b/rakia/media-stream.h @@ -0,0 +1,92 @@ +/* + * sip-media-stream.h - Header for RakiaMediaStream + * Copyright (C) 2005 Collabora Ltd. + * Copyright (C) 2005-2010 Nokia Corporation + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __TPSIP_MEDIA_STREAM_H__ +#define __TPSIP_MEDIA_STREAM_H__ + +#include <glib-object.h> +#include <telepathy-glib/dbus-properties-mixin.h> +#include <telepathy-glib/enums.h> +#include <sofia-sip/sdp.h> + +G_BEGIN_DECLS + +typedef struct _RakiaMediaStream RakiaMediaStream; +typedef struct _RakiaMediaStreamClass RakiaMediaStreamClass; + +struct _RakiaMediaStreamClass { + GObjectClass parent_class; + TpDBusPropertiesMixinClass dbus_props_class; +}; + +struct _RakiaMediaStream { + GObject parent; +}; + +GType rakia_media_stream_get_type(void); + +/* TYPE MACROS */ +#define TPSIP_TYPE_MEDIA_STREAM \ + (rakia_media_stream_get_type()) +#define TPSIP_MEDIA_STREAM(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), TPSIP_TYPE_MEDIA_STREAM, RakiaMediaStream)) +#define TPSIP_MEDIA_STREAM_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), TPSIP_TYPE_MEDIA_STREAM, RakiaMediaStreamClass)) +#define TPSIP_IS_MEDIA_STREAM(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), TPSIP_TYPE_MEDIA_STREAM)) +#define TPSIP_IS_MEDIA_STREAM_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), TPSIP_TYPE_MEDIA_STREAM)) +#define TPSIP_MEDIA_STREAM_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), TPSIP_TYPE_MEDIA_STREAM, RakiaMediaStreamClass)) + +/*********************************************************************** + * Additional declarations (not based on generated templates) + ***********************************************************************/ + +void rakia_media_stream_close (RakiaMediaStream *self); +guint rakia_media_stream_get_id (RakiaMediaStream *self); +guint rakia_media_stream_get_media_type (RakiaMediaStream *self); +const char *rakia_media_stream_local_sdp (RakiaMediaStream *self); +gboolean rakia_media_stream_set_remote_media (RakiaMediaStream *self, + const sdp_media_t *media, + guint direction_up_mask, + guint pending_send_mask); +void rakia_media_stream_set_playing (RakiaMediaStream *self, gboolean playing); +void rakia_media_stream_set_sending (RakiaMediaStream *self, gboolean sending); +void rakia_media_stream_set_direction (RakiaMediaStream *stream, + TpMediaStreamDirection direction, + guint pending_send_mask); +void rakia_media_stream_apply_pending_direction (RakiaMediaStream *stream, + guint pending_send_mask); +TpMediaStreamDirection rakia_media_stream_get_requested_direction (RakiaMediaStream *self); +gboolean rakia_media_stream_is_local_ready (RakiaMediaStream *self); +gboolean rakia_media_stream_is_codec_intersect_pending (RakiaMediaStream *self); +void rakia_media_stream_start_telephony_event (RakiaMediaStream *self, guchar event); +void rakia_media_stream_stop_telephony_event (RakiaMediaStream *self); +gboolean rakia_media_stream_request_hold_state (RakiaMediaStream *self, + gboolean hold); + +guint rakia_tp_media_type (sdp_media_e sip_mtype); +TpMediaStreamDirection rakia_media_stream_direction_from_remote_media ( + const sdp_media_t *media); + +G_END_DECLS + +#endif /* #ifndef __TPSIP_MEDIA_STREAM_H__*/ diff --git a/rakia/signals-marshal.list b/rakia/signals-marshal.list new file mode 100644 index 0000000..ec706c4 --- /dev/null +++ b/rakia/signals-marshal.list @@ -0,0 +1,2 @@ +BOOLEAN:POINTER,POINTER +VOID:UINT,UINT diff --git a/rakia/sofia-decls.h b/rakia/sofia-decls.h new file mode 100644 index 0000000..7f04e24 --- /dev/null +++ b/rakia/sofia-decls.h @@ -0,0 +1,50 @@ +/* + * sofia-decls.h - A header file to pull in Sofia APIs + * Copyright (C) 2006-2009 Nokia Corporation + * @author Mikhail Zabaluev <mikhail.zabaluev@nokia.com> + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _TPSIP_SOFIA_DECLS_H_ +#define _TPSIP_SOFIA_DECLS_H_ + +/* note: As one Sofia-SIP NUA instance is created per SIP connection, + * RakiaConnection is used as the context pointer. + * See {top}/docs/design.txt for further information. + * + * Each NUA handle managed by Telepathy-SofiaSIP is bound to an object + * implementing RakiaEventTarget. The managed NUA handle survives at + * least as long as the bound object. When the object is destroyed, + * the handle is bound to a special end-of-life event handler object. + * Thus, any NUA handle has either the magic value of NULL, or + * pointing to an event target object attached to this handle. + */ + +#define NUA_MAGIC_T struct _RakiaBaseConnection +#define NUA_HMAGIC_T struct _RakiaEventTarget +#define SU_ROOT_MAGIC_T struct _RakiaConnectionManager +#define SU_TIMER_ARG_T struct _RakiaBaseConnection +#define SU_WAKEUP_ARG_T void + +#define TPSIP_DEFAULT_STUN_PORT 3478 + +/* Maximum defer timeout for deferrable Sofia timers */ +#define TPSIP_DEFER_TIMEOUT 30 + +#include <sofia-sip/nua.h> +#include <sofia-sip/su.h> + +#endif /* _TPSIP_SOFIA_DECLS_H_*/ diff --git a/rakia/text-channel.c b/rakia/text-channel.c new file mode 100644 index 0000000..4d4bc8c --- /dev/null +++ b/rakia/text-channel.c @@ -0,0 +1,955 @@ +/* + * sip-text-channel.c - Source for RakiaTextChannel + * Copyright (C) 2005-2008 Collabora Ltd. + * Copyright (C) 2005-2009 Nokia Corporation + * @author Martti Mela <first.surname@nokia.com> + * @author Mikhail Zabaluev <mikhail.zabaluev@nokia.com> + * + * Based on telepathy-gabble implementation (gabble-im-channel). + * @author See gabble-im-channel.c + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include "rakia/text-channel.h" + +#include <stdlib.h> +#include <string.h> +#include <time.h> + +#include <dbus/dbus-glib.h> +#include <telepathy-glib/channel-iface.h> +#include <telepathy-glib/dbus.h> +#include <telepathy-glib/errors.h> +#include <telepathy-glib/gtypes.h> +#include <telepathy-glib/interfaces.h> +#include <telepathy-glib/svc-channel.h> +#include <telepathy-glib/svc-generic.h> + +#include "rakia/event-target.h" +#include "rakia/base-connection.h" + +#include <sofia-sip/sip_protos.h> +#include <sofia-sip/sip_status.h> + +#define DEBUG_FLAG TPSIP_DEBUG_IM +#include "rakia/debug.h" + +static gboolean +rakia_text_channel_nua_r_message_cb (RakiaTextChannel *self, + const RakiaNuaEvent *ev, + tagi_t tags[], + gpointer foo); + +static void channel_iface_init (gpointer, gpointer); +static void destroyable_iface_init (gpointer, gpointer); + +G_DEFINE_TYPE_WITH_CODE (RakiaTextChannel, rakia_text_channel, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (TPSIP_TYPE_EVENT_TARGET, NULL); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_DBUS_PROPERTIES, + tp_dbus_properties_mixin_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL, channel_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_TYPE_TEXT, + tp_message_mixin_text_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_INTERFACE_MESSAGES, + tp_message_mixin_messages_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_INTERFACE_DESTROYABLE, + destroyable_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_EXPORTABLE_CHANNEL, NULL); + G_IMPLEMENT_INTERFACE (TP_TYPE_CHANNEL_IFACE, NULL)); + +static const char *rakia_text_channel_interfaces[] = { + TP_IFACE_CHANNEL_INTERFACE_DESTROYABLE, + NULL +}; + + +/* properties */ +enum +{ + PROP_CONNECTION = 1, + PROP_OBJECT_PATH, + PROP_CHANNEL_TYPE, + PROP_HANDLE_TYPE, + PROP_HANDLE, + PROP_TARGET_ID, + PROP_INITIATOR_HANDLE, + PROP_INITIATOR_ID, + PROP_REQUESTED, + PROP_INTERFACES, + PROP_CHANNEL_DESTROYED, + PROP_CHANNEL_PROPERTIES, + LAST_PROPERTY +}; + + +/* private structures */ + +typedef struct _RakiaTextPendingMessage RakiaTextPendingMessage; + +struct _RakiaTextPendingMessage +{ + nua_handle_t *nh; + gchar *token; + TpMessageSendingFlags flags; +}; + +typedef struct _RakiaTextChannelPrivate RakiaTextChannelPrivate; + +struct _RakiaTextChannelPrivate +{ + RakiaBaseConnection *conn; + gchar *object_path; + TpHandle handle; + TpHandle initiator; + + guint sent_id; + GQueue *sending_messages; + + gboolean closed; + + gboolean dispose_has_run; +}; + + +#define _rakia_text_pending_new0() \ + (g_slice_new0(RakiaTextPendingMessage)) + +#define TPSIP_TEXT_CHANNEL_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), TPSIP_TYPE_TEXT_CHANNEL, RakiaTextChannelPrivate)) + +static void rakia_text_pending_free (RakiaTextPendingMessage *msg, + TpHandleRepoIface *contact_handles) +{ + if (msg->nh) + nua_handle_unref (msg->nh); + + g_free (msg->token); + + g_slice_free (RakiaTextPendingMessage, msg); +} + +static void +rakia_text_channel_init (RakiaTextChannel *obj) +{ + RakiaTextChannelPrivate *priv = TPSIP_TEXT_CHANNEL_GET_PRIVATE (obj); + + DEBUG("enter"); + + priv->sending_messages = g_queue_new (); +} + +static void rakia_text_channel_send_message (GObject *object, + TpMessage *message, + TpMessageSendingFlags flags); + +static void +rakia_text_channel_constructed (GObject *obj) +{ + RakiaTextChannelPrivate *priv; + TpBaseConnection *base_conn; + TpHandleRepoIface *contact_handles; + TpDBusDaemon *bus; + TpChannelTextMessageType types[] = { + TP_CHANNEL_TEXT_MESSAGE_TYPE_NORMAL, + }; + const gchar * supported_content_types[] = { + "text/plain", + NULL + }; + GObjectClass *parent_object_class = + G_OBJECT_CLASS (rakia_text_channel_parent_class); + + if (parent_object_class->constructed != NULL) + parent_object_class->constructed (obj); + + priv = TPSIP_TEXT_CHANNEL_GET_PRIVATE(TPSIP_TEXT_CHANNEL(obj)); + base_conn = (TpBaseConnection *) priv->conn; + contact_handles = tp_base_connection_get_handles (base_conn, + TP_HANDLE_TYPE_CONTACT); + + tp_handle_ref (contact_handles, priv->handle); + + g_assert (priv->initiator != 0); + tp_handle_ref (contact_handles, priv->initiator); + + rakia_base_connection_add_auth_handler (priv->conn, TPSIP_EVENT_TARGET (obj)); + + g_signal_connect (obj, + "nua-event::nua_r_message", + G_CALLBACK (rakia_text_channel_nua_r_message_cb), + NULL); + + tp_message_mixin_init (obj, G_STRUCT_OFFSET (RakiaTextChannel, message_mixin), + base_conn); + + tp_message_mixin_implement_sending (obj, rakia_text_channel_send_message, + G_N_ELEMENTS (types), types, 0, + TP_DELIVERY_REPORTING_SUPPORT_FLAG_RECEIVE_FAILURES | + TP_DELIVERY_REPORTING_SUPPORT_FLAG_RECEIVE_SUCCESSES, + supported_content_types); + + bus = tp_base_connection_get_dbus_daemon (base_conn); + tp_dbus_daemon_register_object (bus, priv->object_path, obj); +} + + +static void rakia_text_channel_get_property(GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec); +static void rakia_text_channel_set_property(GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec); +static void rakia_text_channel_dispose(GObject *object); +static void rakia_text_channel_finalize(GObject *object); + +static void +rakia_text_channel_class_init(RakiaTextChannelClass *klass) +{ + static TpDBusPropertiesMixinPropImpl channel_props[] = { + { "ChannelType", "channel-type", NULL }, + { "Interfaces", "interfaces", NULL }, + { "TargetHandleType", "handle-type", NULL }, + { "TargetHandle", "handle", NULL }, + { "TargetID", "target-id", NULL }, + { "InitiatorHandle", "initiator-handle", NULL }, + { "InitiatorID", "initiator-id", NULL }, + { "Requested", "requested", NULL }, + { NULL } + }; + static TpDBusPropertiesMixinIfaceImpl prop_interfaces[] = { + { TP_IFACE_CHANNEL, + tp_dbus_properties_mixin_getter_gobject_properties, + NULL, + channel_props, + }, + { NULL } + }; + + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GParamSpec *param_spec; + + DEBUG("enter"); + + g_type_class_add_private (klass, sizeof (RakiaTextChannelPrivate)); + + object_class->get_property = rakia_text_channel_get_property; + object_class->set_property = rakia_text_channel_set_property; + + object_class->constructed = rakia_text_channel_constructed; + + object_class->dispose = rakia_text_channel_dispose; + object_class->finalize = rakia_text_channel_finalize; + + g_object_class_override_property (object_class, PROP_HANDLE_TYPE, + "handle-type"); + g_object_class_override_property (object_class, PROP_HANDLE, "handle"); + g_object_class_override_property (object_class, PROP_OBJECT_PATH, + "object-path"); + g_object_class_override_property (object_class, PROP_CHANNEL_TYPE, + "channel-type"); + + g_object_class_override_property (object_class, PROP_CHANNEL_DESTROYED, + "channel-destroyed"); + g_object_class_override_property (object_class, PROP_CHANNEL_PROPERTIES, + "channel-properties"); + + param_spec = g_param_spec_object("connection", "RakiaConnection object", + "SIP connection object that owns this SIP media channel object.", + TPSIP_TYPE_BASE_CONNECTION, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property(object_class, PROP_CONNECTION, param_spec); + + param_spec = g_param_spec_boxed ("interfaces", "Extra D-Bus interfaces", + "Addition Channel.Interface.* interfaces", G_TYPE_STRV, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_INTERFACES, param_spec); + + param_spec = g_param_spec_string ("target-id", "Peer's SIP URI", + "The URI string obtained by inspecting the peer handle", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_TARGET_ID, param_spec); + + param_spec = g_param_spec_uint ("initiator-handle", "Initiator's handle", + "The contact who initiated the channel", + 0, G_MAXUINT32, 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_INITIATOR_HANDLE, + param_spec); + + param_spec = g_param_spec_string ("initiator-id", "Initiator's URI", + "The string obtained by inspecting the initiator-handle", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_INITIATOR_ID, + param_spec); + + param_spec = g_param_spec_boolean ("requested", "Requested?", + "True if this channel was requested by the local user", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_REQUESTED, param_spec); + + klass->dbus_props_class.interfaces = + prop_interfaces; + tp_dbus_properties_mixin_class_init (object_class, + G_STRUCT_OFFSET (RakiaTextChannelClass, dbus_props_class)); + + tp_message_mixin_init_dbus_properties (object_class); +} + +static void +rakia_text_channel_get_property(GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + RakiaTextChannel *chan = TPSIP_TEXT_CHANNEL(object); + RakiaTextChannelPrivate *priv = TPSIP_TEXT_CHANNEL_GET_PRIVATE(chan); + TpBaseConnection *base_conn = (TpBaseConnection *) priv->conn; + + switch (property_id) + { + case PROP_CONNECTION: + g_value_set_object(value, priv->conn); + break; + + case PROP_OBJECT_PATH: + g_value_set_string(value, priv->object_path); + break; + + case PROP_CHANNEL_TYPE: + g_value_set_string(value, TP_IFACE_CHANNEL_TYPE_TEXT); + break; + + case PROP_HANDLE_TYPE: + g_value_set_uint(value, TP_HANDLE_TYPE_CONTACT); + break; + + case PROP_HANDLE: + g_value_set_uint(value, priv->handle); + break; + + case PROP_TARGET_ID: + { + TpHandleRepoIface *repo = tp_base_connection_get_handles ( + base_conn, TP_HANDLE_TYPE_CONTACT); + + g_value_set_string (value, tp_handle_inspect (repo, priv->handle)); + } + break; + + case PROP_INITIATOR_HANDLE: + g_value_set_uint (value, priv->initiator); + break; + + case PROP_INITIATOR_ID: + { + TpHandleRepoIface *repo = tp_base_connection_get_handles ( + base_conn, TP_HANDLE_TYPE_CONTACT); + + g_assert (priv->initiator != 0); + g_value_set_string (value, + tp_handle_inspect (repo, priv->initiator)); + } + break; + + case PROP_REQUESTED: + g_value_set_boolean (value, (priv->initiator == base_conn->self_handle)); + break; + + case PROP_CHANNEL_DESTROYED: + g_value_set_boolean (value, priv->closed); + break; + + case PROP_CHANNEL_PROPERTIES: + g_value_take_boxed (value, + tp_dbus_properties_mixin_make_properties_hash (object, + TP_IFACE_CHANNEL, "ChannelType", + TP_IFACE_CHANNEL, "TargetHandleType", + TP_IFACE_CHANNEL, "TargetHandle", + TP_IFACE_CHANNEL, "TargetID", + TP_IFACE_CHANNEL, "InitiatorHandle", + TP_IFACE_CHANNEL, "InitiatorID", + TP_IFACE_CHANNEL, "Requested", + TP_IFACE_CHANNEL, "Interfaces", + NULL)); + break; + + case PROP_INTERFACES: + g_value_set_static_boxed (value, rakia_text_channel_interfaces); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + } +} + +static void +rakia_text_channel_set_property(GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + RakiaTextChannel *chan = TPSIP_TEXT_CHANNEL (object); + RakiaTextChannelPrivate *priv = TPSIP_TEXT_CHANNEL_GET_PRIVATE (chan); + + switch (property_id) + { + case PROP_CONNECTION: + priv->conn = g_value_get_object (value); + break; + + case PROP_OBJECT_PATH: + g_assert (priv->object_path == NULL); + priv->object_path = g_value_dup_string (value); + break; + + case PROP_CHANNEL_TYPE: + case PROP_HANDLE_TYPE: + /* this property is writable in the interface, but not actually + * meaningfully changable on this channel, so we do nothing */ + break; + + case PROP_HANDLE: + /* we don't ref it here because we don't necessarily have access to the + * contact repo yet - instead we ref it in the constructed */ + priv->handle = g_value_get_uint(value); + break; + + case PROP_INITIATOR_HANDLE: + /* similarly we can't ref this yet */ + priv->initiator = g_value_get_uint (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +rakia_text_channel_dispose(GObject *object) +{ + RakiaTextChannel *self = TPSIP_TEXT_CHANNEL (object); + RakiaTextChannelPrivate *priv = TPSIP_TEXT_CHANNEL_GET_PRIVATE (self); + TpHandleRepoIface *contact_handles; + + if (priv->dispose_has_run) + return; + + priv->dispose_has_run = TRUE; + + if (!priv->closed) + { + priv->closed = TRUE; + tp_svc_channel_emit_closed (self); + } + + contact_handles = tp_base_connection_get_handles ( + (TpBaseConnection *) priv->conn, TP_HANDLE_TYPE_CONTACT); + + tp_handle_unref (contact_handles, priv->handle); + + if (priv->initiator != 0) + tp_handle_unref (contact_handles, priv->initiator); + + if (G_OBJECT_CLASS (rakia_text_channel_parent_class)->dispose) + G_OBJECT_CLASS (rakia_text_channel_parent_class)->dispose (object); +} + +static void +zap_pending_messages (GQueue *pending_messages, + TpHandleRepoIface *contact_handles) +{ + g_queue_foreach (pending_messages, + (GFunc) rakia_text_pending_free, contact_handles); + g_queue_clear (pending_messages); +} + +static void +rakia_text_channel_finalize(GObject *object) +{ + RakiaTextChannel *self = TPSIP_TEXT_CHANNEL (object); + RakiaTextChannelPrivate *priv = TPSIP_TEXT_CHANNEL_GET_PRIVATE (self); + TpHandleRepoIface *contact_handles; + + contact_handles = tp_base_connection_get_handles ( + (TpBaseConnection *)priv->conn, TP_HANDLE_TYPE_CONTACT); + + DEBUG ("%u pending outgoing message requests", + g_queue_get_length (priv->sending_messages)); + zap_pending_messages (priv->sending_messages, contact_handles); + g_queue_free (priv->sending_messages); + + g_free (priv->object_path); + + tp_message_mixin_finalize (object); + + G_OBJECT_CLASS (rakia_text_channel_parent_class)->finalize (object); +} + +static gint rakia_acknowledged_messages_compare(gconstpointer msg, + gconstpointer id) +{ + RakiaTextPendingMessage *message = (RakiaTextPendingMessage *)msg; + nua_handle_t *nh = (nua_handle_t *) id; + return (message->nh != nh); +} + +/** + * rakia_text_channel_close + * + * Implements DBus method Close + * on interface org.freedesktop.Telepathy.Channel + */ +static void +rakia_text_channel_close (TpSvcChannel *iface, + DBusGMethodInvocation *context) +{ + RakiaTextChannel *self = TPSIP_TEXT_CHANNEL (iface); + RakiaTextChannelPrivate *priv = TPSIP_TEXT_CHANNEL_GET_PRIVATE(self); + + if (priv->closed) + { + DEBUG ("already closed, doing nothing"); + } + else + { + if (!tp_message_mixin_has_pending_messages ((GObject *) self, NULL)) + { + DEBUG ("actually closing, no pending messages"); + priv->closed = TRUE; + } + else + { + DEBUG ("not really closing, there are pending messages left"); + + tp_message_mixin_set_rescued ((GObject *) self); + + if (priv->initiator != priv->handle) + { + TpHandleRepoIface *contact_repo = tp_base_connection_get_handles + ((TpBaseConnection *) priv->conn, TP_HANDLE_TYPE_CONTACT); + + g_assert (priv->initiator != 0); + g_assert (priv->handle != 0); + + tp_handle_unref (contact_repo, priv->initiator); + priv->initiator = priv->handle; + tp_handle_ref (contact_repo, priv->initiator); + } + } + tp_svc_channel_emit_closed (self); + } + tp_svc_channel_return_from_close (context); +} + +/** + * rakia_text_channel_destroy + * + * Implements D-Bus method Destroy + * on interface org.freedesktop.Telepathy.Channel.Interface.Destroyable + */ +static void +rakia_text_channel_destroy (TpSvcChannelInterfaceDestroyable *iface, + DBusGMethodInvocation *context) +{ + RakiaTextChannel *self = TPSIP_TEXT_CHANNEL (iface); + RakiaTextChannelPrivate *priv = TPSIP_TEXT_CHANNEL_GET_PRIVATE (self); + TpHandleRepoIface *contact_handles; + + contact_handles = tp_base_connection_get_handles ( + (TpBaseConnection *) priv->conn, TP_HANDLE_TYPE_CONTACT); + + tp_message_mixin_clear ((GObject *) iface); + + /* Close() and Destroy() have the same signature, so we can safely + * chain to the other function now */ + rakia_text_channel_close ((TpSvcChannel *) self, context); +} + +/** + * rakia_text_channel_get_channel_type + * + * Implements DBus method GetChannelType + * on interface org.freedesktop.Telepathy.Channel + */ +static void +rakia_text_channel_get_channel_type (TpSvcChannel *iface, + DBusGMethodInvocation *context) +{ + DEBUG("enter"); + + tp_svc_channel_return_from_get_channel_type (context, + TP_IFACE_CHANNEL_TYPE_TEXT); +} + + +/** + * rakia_text_channel_get_handle + * + * Implements DBus method GetHandle + * on interface org.freedesktop.Telepathy.Channel + */ +static void +rakia_text_channel_get_handle (TpSvcChannel *iface, + DBusGMethodInvocation *context) +{ + RakiaTextChannel *obj = TPSIP_TEXT_CHANNEL (iface); + RakiaTextChannelPrivate *priv; + + DEBUG("enter"); + + priv = TPSIP_TEXT_CHANNEL_GET_PRIVATE(obj); + + tp_svc_channel_return_from_get_handle (context, TP_HANDLE_TYPE_CONTACT, + priv->handle); +} + + +/** + * rakia_text_channel_get_interfaces + * + * Implements DBus method GetInterfaces + * on interface org.freedesktop.Telepathy.Channel + */ +static void +rakia_text_channel_get_interfaces(TpSvcChannel *iface, + DBusGMethodInvocation *context) +{ + DEBUG("enter"); + tp_svc_channel_return_from_get_interfaces (context, + rakia_text_channel_interfaces); +} + +static void +rakia_text_channel_send_message (GObject *object, + TpMessage *message, + TpMessageSendingFlags flags) +{ + RakiaTextChannel *self = TPSIP_TEXT_CHANNEL(object); + RakiaTextChannelPrivate *priv = TPSIP_TEXT_CHANNEL_GET_PRIVATE (self); + RakiaTextPendingMessage *msg = NULL; + nua_handle_t *msg_nh = NULL; + GError *error = NULL; + const GHashTable *part; + guint n_parts; + const gchar *content_type; + const gchar *text; + + DEBUG("enter"); + +#define INVALID_ARGUMENT(msg, ...) \ + G_STMT_START { \ + DEBUG (msg , ## __VA_ARGS__); \ + g_set_error (&error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, \ + msg , ## __VA_ARGS__); \ + goto fail; \ + } G_STMT_END + + part = tp_message_peek (message, 0); + + if (tp_asv_lookup (part, "message-type") != NULL) + { + if (tp_asv_get_uint32 (part, "message-type", NULL) != + TP_CHANNEL_TEXT_MESSAGE_TYPE_NORMAL) + INVALID_ARGUMENT ("invalid message type"); + } + + n_parts = tp_message_count_parts (message); + + if (n_parts != 2) + INVALID_ARGUMENT ("message must contain exactly 1 part, not %u", + (n_parts - 1)); + + part = tp_message_peek (message, 1); + content_type = tp_asv_get_string (part, "content-type"); + text = tp_asv_get_string (part, "content"); + + if (content_type == NULL || tp_strdiff (content_type, "text/plain")) + INVALID_ARGUMENT ("message must be text/plain"); + + if (text == NULL) + INVALID_ARGUMENT ("content must be a UTF-8 string"); + + /* Okay, it's valid. Let's send it. */ + + msg_nh = rakia_base_connection_create_handle (priv->conn, priv->handle); + if (msg_nh == NULL) + { + g_set_error (&error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "Request creation failed"); + goto fail; + } + + rakia_event_target_attach (msg_nh, (GObject *) self); + + nua_message(msg_nh, + SIPTAG_CONTENT_TYPE_STR("text/plain"), + SIPTAG_PAYLOAD_STR(text), + TAG_END()); + + msg = _rakia_text_pending_new0 (); + msg->nh = msg_nh; + msg->token = g_strdup_printf ("%u", priv->sent_id++); + msg->flags = flags; + + tp_message_mixin_sent (object, message, flags, msg->token, NULL); + g_queue_push_tail (priv->sending_messages, msg); + + DEBUG ("message queued for delivery"); + return; + +fail: + g_assert (error != NULL); + tp_message_mixin_sent (object, message, 0, NULL, error); + g_error_free (error); +} + +static gchar * +text_send_error_to_dbus_error (TpChannelTextSendError error) +{ + switch (error) + { + case TP_CHANNEL_TEXT_SEND_ERROR_OFFLINE: + return TP_ERROR_STR_OFFLINE; + + case TP_CHANNEL_TEXT_SEND_ERROR_INVALID_CONTACT: + return TP_ERROR_STR_INVALID_HANDLE; + + case TP_CHANNEL_TEXT_SEND_ERROR_PERMISSION_DENIED: + return TP_ERROR_STR_PERMISSION_DENIED; + + case TP_CHANNEL_TEXT_SEND_ERROR_TOO_LONG: + return TP_ERROR_STR_INVALID_ARGUMENT; + + case TP_CHANNEL_TEXT_SEND_ERROR_NOT_IMPLEMENTED: + return TP_ERROR_STR_NOT_IMPLEMENTED; + + case TP_CHANNEL_TEXT_SEND_ERROR_UNKNOWN: + default: + return TP_ERROR_STR_INVALID_ARGUMENT; + } + + return NULL; +} + +static void +delivery_report (RakiaTextChannel *self, + const gchar *token, + TpDeliveryStatus status, + TpChannelTextSendError send_error) +{ + RakiaTextChannelPrivate *priv = TPSIP_TEXT_CHANNEL_GET_PRIVATE (self); + TpBaseConnection *base_conn; + TpMessage *msg; + + base_conn = (TpBaseConnection *) priv->conn; + + msg = tp_message_new (base_conn, 1, 1); + + tp_message_set_handle (msg, 0, "message-sender", TP_HANDLE_TYPE_CONTACT, + priv->handle); + + tp_message_set_uint32 (msg, 0, "message-type", + TP_CHANNEL_TEXT_MESSAGE_TYPE_DELIVERY_REPORT); + + tp_message_set_string (msg, 0, "delivery-token", token); + tp_message_set_uint32 (msg, 0, "delivery-status", status); + + if (status == TP_DELIVERY_STATUS_TEMPORARILY_FAILED || + status == TP_DELIVERY_STATUS_PERMANENTLY_FAILED) + { + if (send_error != TP_CHANNEL_TEXT_SEND_ERROR_UNKNOWN) + tp_message_set_uint32 (msg, 0, "delivery-error", send_error); + + tp_message_set_string (msg, 0, "delivery-dbus-error", + text_send_error_to_dbus_error (send_error)); + } + + tp_message_mixin_take_received((GObject *) self, msg); +} + +static gboolean +rakia_text_channel_nua_r_message_cb (RakiaTextChannel *self, + const RakiaNuaEvent *ev, + tagi_t tags[], + gpointer foo) +{ + RakiaTextChannelPrivate *priv = TPSIP_TEXT_CHANNEL_GET_PRIVATE (self); + RakiaTextPendingMessage *msg; + TpHandleRepoIface *contact_repo; + TpChannelTextSendError send_error; + GList *node; + + /* ignore provisional responses */ + if (ev->status < 200) + return TRUE; + + node = g_queue_find_custom (priv->sending_messages, + ev->nua_handle, + rakia_acknowledged_messages_compare); + + /* Shouldn't happen... */ + if (node == NULL) + { + WARNING ("message pending sent acknowledgement not found"); + return FALSE; + } + + msg = (RakiaTextPendingMessage *)node->data; + + g_assert (msg != NULL); + + /* FIXME: generate a delivery report */ + if (ev->status >= 200 && ev->status < 300) + { + DEBUG ("message delivered"); + + if (msg->flags & TP_MESSAGE_SENDING_FLAG_REPORT_DELIVERY) + { + DEBUG ("Sending delivery report"); + delivery_report (self, msg->token, TP_DELIVERY_STATUS_DELIVERED, 0); + } + } + else + { + switch (ev->status) + { + case 401: + case 403: + case 407: + case 603: + send_error = TP_CHANNEL_TEXT_SEND_ERROR_PERMISSION_DENIED; + break; + case 604: + send_error = TP_CHANNEL_TEXT_SEND_ERROR_INVALID_CONTACT; + break; + case 405: + case 406: + case 415: + case 416: + case 488: + case 501: + case 505: + case 606: + send_error = TP_CHANNEL_TEXT_SEND_ERROR_NOT_IMPLEMENTED; + break; + case 410: + send_error = TP_CHANNEL_TEXT_SEND_ERROR_INVALID_CONTACT /* TP_CHANNEL_TEXT_SEND_ERROR_OFFLINE? */; + break; + case 404: + case 480: + send_error = TP_CHANNEL_TEXT_SEND_ERROR_OFFLINE; + break; + case 413: + case 513: + send_error = TP_CHANNEL_TEXT_SEND_ERROR_TOO_LONG; + break; + default: + send_error = TP_CHANNEL_TEXT_SEND_ERROR_UNKNOWN; + } + + delivery_report (self, msg->token, TP_DELIVERY_STATUS_PERMANENTLY_FAILED, + send_error); + } + + g_queue_remove(priv->sending_messages, msg); + + contact_repo = tp_base_connection_get_handles ( + (TpBaseConnection *)(priv->conn), TP_HANDLE_TYPE_CONTACT); + + rakia_text_pending_free(msg, contact_repo); + + return TRUE; +} + +void rakia_text_channel_receive(RakiaTextChannel *chan, + const sip_t *sip, + TpHandle sender, + const char *text, + gsize len) +{ + RakiaTextChannelPrivate *priv = TPSIP_TEXT_CHANNEL_GET_PRIVATE (chan); + TpMessage *msg; + TpBaseConnection *base_conn; + sip_call_id_t *hdr_call_id; + sip_cseq_t *hdr_cseq; + sip_date_t *hdr_date_sent; + + base_conn = (TpBaseConnection *) priv->conn; + msg = tp_message_new (base_conn, 2, 2); + + DEBUG ("Received message from contact %u: %s", sender, text); + + /* Header */ + tp_message_set_handle (msg, 0, "message-sender", TP_HANDLE_TYPE_CONTACT, + sender); + tp_message_set_int64 (msg, 0, "message-received", time (NULL)); + + hdr_date_sent = sip_date (sip); + if (hdr_date_sent != NULL) + { + tp_message_set_int64 (msg, 0, "message-sent", + hdr_date_sent->d_time - SU_TIME_EPOCH); + } + + /* Create a message token out of globally unique SIP header values. + * As MESSAGE requests can be sent within a dialog, we have to append + * the Call-ID value with the sequence number in CSeq. */ + hdr_call_id = sip_call_id (sip); + hdr_cseq = sip_cseq (sip); + if (hdr_call_id != NULL && hdr_cseq != NULL) + { + tp_message_set_string_printf (msg, 0, "message-token", "%s;cseq=%u", + hdr_call_id->i_id, (guint) hdr_cseq->cs_seq); + } + + /* Body */ + tp_message_set_string (msg, 1, "content-type", "text/plain"); + tp_message_set_string (msg, 1, "content", text); + + tp_message_mixin_take_received (G_OBJECT (chan), msg); +} + +static void +destroyable_iface_init (gpointer g_iface, + gpointer iface_data) +{ + TpSvcChannelInterfaceDestroyableClass *klass = g_iface; + +#define IMPLEMENT(x) tp_svc_channel_interface_destroyable_implement_##x (\ + klass, rakia_text_channel_##x) + IMPLEMENT(destroy); +#undef IMPLEMENT +} + +static void +channel_iface_init(gpointer g_iface, gpointer iface_data) +{ + TpSvcChannelClass *klass = (TpSvcChannelClass *)g_iface; + +#define IMPLEMENT(x) tp_svc_channel_implement_##x (\ + klass, rakia_text_channel_##x) + IMPLEMENT(close); + IMPLEMENT(get_channel_type); + IMPLEMENT(get_handle); + IMPLEMENT(get_interfaces); +#undef IMPLEMENT +} diff --git a/rakia/text-channel.h b/rakia/text-channel.h new file mode 100644 index 0000000..e74eb1c --- /dev/null +++ b/rakia/text-channel.h @@ -0,0 +1,72 @@ +/* + * sip-text-channel.h - Header for RakiaTextChannel + * Copyright (C) 2005-2008 Collabora Ltd. + * Copyright (C) 2005-2009 Nokia Corporation + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __TPSIP_TEXT_CHANNEL_H__ +#define __TPSIP_TEXT_CHANNEL_H__ + +#include <glib-object.h> +#include <telepathy-glib/dbus-properties-mixin.h> +#include <telepathy-glib/handle.h> +#include <telepathy-glib/message-mixin.h> + +#include <rakia/sofia-decls.h> + +G_BEGIN_DECLS + +typedef struct _RakiaTextChannel RakiaTextChannel; +typedef struct _RakiaTextChannelClass RakiaTextChannelClass; + +struct _RakiaTextChannelClass { + GObjectClass parent_class; + + TpDBusPropertiesMixinClass dbus_props_class; +}; + +struct _RakiaTextChannel { + GObject parent; + + TpMessageMixin message_mixin; +}; + +GType rakia_text_channel_get_type(void); + +/* TYPE MACROS */ +#define TPSIP_TYPE_TEXT_CHANNEL \ + (rakia_text_channel_get_type()) +#define TPSIP_TEXT_CHANNEL(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), TPSIP_TYPE_TEXT_CHANNEL, RakiaTextChannel)) +#define TPSIP_TEXT_CHANNEL_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), TPSIP_TYPE_TEXT_CHANNEL, RakiaTextChannelClass)) +#define TPSIP_IS_TEXT_CHANNEL(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), TPSIP_TYPE_TEXT_CHANNEL)) +#define TPSIP_IS_TEXT_CHANNEL_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), TPSIP_TYPE_TEXT_CHANNEL)) +#define TPSIP_TEXT_CHANNEL_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), TPSIP_TYPE_TEXT_CHANNEL, RakiaTextChannelClass)) + +void rakia_text_channel_receive (RakiaTextChannel *obj, + const sip_t *sip, + TpHandle sender, + const char *text, + gsize len); + +G_END_DECLS + +#endif /* #ifndef __TPSIP_TEXT_CHANNEL_H__*/ diff --git a/rakia/text-manager.c b/rakia/text-manager.c new file mode 100644 index 0000000..731694f --- /dev/null +++ b/rakia/text-manager.c @@ -0,0 +1,642 @@ +/* + * text-manager.c - Text channel manager for SIP + * Copyright (C) 2007-2008 Collabora Ltd. + * Copyright (C) 2007-2011 Nokia Corporation + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include "rakia/text-manager.h" + +#include <string.h> + +#include <telepathy-glib/channel-manager.h> +#include <telepathy-glib/dbus.h> +#include <telepathy-glib/interfaces.h> + +#include "rakia/text-channel.h" +#include "rakia/base-connection.h" +#include "rakia/handles.h" + +#include <sofia-sip/msg_header.h> +#include <sofia-sip/sip_tag.h> +#include <sofia-sip/sip_status.h> + +#define DEBUG_FLAG TPSIP_DEBUG_IM +#include "rakia/debug.h" + + +static void channel_manager_iface_init (gpointer g_iface, gpointer iface_data); +static void connection_status_changed_cb (TpBaseConnection *conn, + guint status, guint reason, RakiaTextManager *self); +static void rakia_text_manager_close_all (RakiaTextManager *self); + +G_DEFINE_TYPE_WITH_CODE (RakiaTextManager, rakia_text_manager, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (TP_TYPE_CHANNEL_MANAGER, + channel_manager_iface_init)) + +enum +{ + PROP_CONNECTION = 1, + LAST_PROPERTY +}; + +typedef struct _RakiaTextManagerPrivate RakiaTextManagerPrivate; +struct _RakiaTextManagerPrivate +{ + TpBaseConnection *conn; + /* guint handle => RakiaTextChannel *channel */ + GHashTable *channels; + + gulong status_changed_id; + gulong message_received_id; + + gboolean dispose_has_run; +}; + +#define TPSIP_TEXT_MANAGER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), TPSIP_TYPE_TEXT_MANAGER, RakiaTextManagerPrivate)) + +static void +rakia_text_manager_init (RakiaTextManager *fac) +{ + RakiaTextManagerPrivate *priv = TPSIP_TEXT_MANAGER_GET_PRIVATE (fac); + + priv->conn = NULL; + priv->channels = g_hash_table_new_full (g_direct_hash, g_direct_equal, + NULL, g_object_unref); + + priv->dispose_has_run = FALSE; +} + +static void +rakia_text_manager_constructed (GObject *object) +{ + RakiaTextManager *fac = TPSIP_TEXT_MANAGER (object); + RakiaTextManagerPrivate *priv = TPSIP_TEXT_MANAGER_GET_PRIVATE (fac); + GObjectClass *parent_object_class = + G_OBJECT_CLASS (rakia_text_manager_parent_class); + + if (parent_object_class->constructed != NULL) + parent_object_class->constructed (object); + + priv->status_changed_id = g_signal_connect (priv->conn, + "status-changed", (GCallback) connection_status_changed_cb, object); +} + +static void +rakia_text_manager_dispose (GObject *object) +{ + RakiaTextManager *fac = TPSIP_TEXT_MANAGER (object); + RakiaTextManagerPrivate *priv = TPSIP_TEXT_MANAGER_GET_PRIVATE (fac); + + if (priv->dispose_has_run) + return; + + priv->dispose_has_run = TRUE; + + rakia_text_manager_close_all (fac); + g_assert (priv->channels == NULL); + + if (G_OBJECT_CLASS (rakia_text_manager_parent_class)->dispose) + G_OBJECT_CLASS (rakia_text_manager_parent_class)->dispose (object); +} + +static void +rakia_text_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + RakiaTextManager *fac = TPSIP_TEXT_MANAGER (object); + RakiaTextManagerPrivate *priv = TPSIP_TEXT_MANAGER_GET_PRIVATE (fac); + + switch (property_id) { + case PROP_CONNECTION: + g_value_set_object (value, priv->conn); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +rakia_text_manager_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + RakiaTextManager *fac = TPSIP_TEXT_MANAGER (object); + RakiaTextManagerPrivate *priv = TPSIP_TEXT_MANAGER_GET_PRIVATE (fac); + + switch (property_id) { + case PROP_CONNECTION: + priv->conn = TP_BASE_CONNECTION (g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +rakia_text_manager_class_init (RakiaTextManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GParamSpec *param_spec; + + g_type_class_add_private (klass, sizeof (RakiaTextManagerPrivate)); + + object_class->constructed = rakia_text_manager_constructed; + object_class->get_property = rakia_text_manager_get_property; + object_class->set_property = rakia_text_manager_set_property; + object_class->dispose = rakia_text_manager_dispose; + + param_spec = g_param_spec_object ("connection", + "RakiaBaseConnection object", + "SIP connection that owns this text channel manager", + TPSIP_TYPE_BASE_CONNECTION, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_CONNECTION, param_spec); +} + +static void +rakia_text_manager_close_all (RakiaTextManager *fac) +{ + RakiaTextManagerPrivate *priv = TPSIP_TEXT_MANAGER_GET_PRIVATE (fac); + GHashTable *channels; + + if (priv->status_changed_id != 0) + { + g_signal_handler_disconnect (priv->conn, + priv->status_changed_id); + priv->status_changed_id = 0; + } + + if (!priv->channels) + return; + + channels = priv->channels; + priv->channels = NULL; + + g_hash_table_destroy (channels); +} + +struct _ForeachData +{ + TpExportableChannelFunc func; + gpointer user_data; +}; + +static void +_foreach_slave (gpointer key, gpointer value, gpointer user_data) +{ + struct _ForeachData *data = (struct _ForeachData *)user_data; + TpExportableChannel *chan = TP_EXPORTABLE_CHANNEL (value); + + data->func (chan, data->user_data); +} + +static void +rakia_text_manager_foreach_channel (TpChannelManager *manager, + TpExportableChannelFunc func, + gpointer user_data) +{ + RakiaTextManager *fac = TPSIP_TEXT_MANAGER (manager); + RakiaTextManagerPrivate *priv = TPSIP_TEXT_MANAGER_GET_PRIVATE (fac); + struct _ForeachData data; + + data.func = func; + data.user_data = user_data; + + g_hash_table_foreach (priv->channels, _foreach_slave, &data); +} + +/** + * text_channel_closed_cb: + * + * Signal callback for when a text channel is closed. Removes the references + * that #RakiaChannelManager holds to them. + */ +static void +channel_closed (RakiaTextChannel *chan, gpointer user_data) +{ + RakiaTextManager *self = TPSIP_TEXT_MANAGER (user_data); + RakiaTextManagerPrivate *priv = TPSIP_TEXT_MANAGER_GET_PRIVATE (self); + TpHandle contact_handle; + gboolean really_destroyed = TRUE; + + tp_channel_manager_emit_channel_closed_for_object (self, + (TpExportableChannel *) chan); + + if (priv->channels == NULL) + return; + + g_object_get (chan, + "handle", &contact_handle, + "channel-destroyed", &really_destroyed, + NULL); + + if (really_destroyed) + { + DEBUG ("removing text channel with handle %u", contact_handle); + g_hash_table_remove (priv->channels, GINT_TO_POINTER (contact_handle)); + } + else + { + DEBUG ("reopening channel with handle %u due to pending messages", + contact_handle); + tp_channel_manager_emit_new_channel (self, + (TpExportableChannel *) chan, NULL); + } +} + +/** + * new_text_channel + * + * Creates a new empty RakiaTextChannel. + */ +static RakiaTextChannel * +rakia_text_manager_new_channel (RakiaTextManager *fac, + TpHandle handle, + TpHandle initiator, + gpointer request_token) +{ + RakiaTextManagerPrivate *priv; + RakiaTextChannel *chan; + gchar *object_path; + TpBaseConnection *conn; + GSList *request_tokens; + + priv = TPSIP_TEXT_MANAGER_GET_PRIVATE (fac); + conn = priv->conn; + + object_path = g_strdup_printf ("%s/TextChannel%u", + conn->object_path, handle); + + DEBUG ("object path %s", object_path); + + chan = g_object_new (TPSIP_TYPE_TEXT_CHANNEL, + "connection", priv->conn, + "object-path", object_path, + "handle", handle, + "initiator-handle", initiator, + NULL); + + g_free (object_path); + + g_signal_connect (chan, "closed", (GCallback) channel_closed, fac); + + g_hash_table_insert (priv->channels, GUINT_TO_POINTER (handle), chan); + + if (request_token != NULL) + request_tokens = g_slist_prepend (NULL, request_token); + else + request_tokens = NULL; + + tp_channel_manager_emit_new_channel (fac, + (TpExportableChannel *) chan, request_tokens); + + g_slist_free (request_tokens); + + return chan; +} + + +static const gchar * const text_channel_fixed_properties[] = { + TP_IFACE_CHANNEL ".ChannelType", + TP_IFACE_CHANNEL ".TargetHandleType", + NULL +}; + +static const gchar * const text_channel_allowed_properties[] = { + TP_IFACE_CHANNEL ".TargetHandle", + TP_IFACE_CHANNEL ".TargetID", + NULL +}; + +static void +rakia_text_manager_type_foreach_channel_class (GType type, + TpChannelManagerTypeChannelClassFunc func, + gpointer user_data) +{ + GHashTable *table = g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, (GDestroyNotify) tp_g_value_slice_free); + GValue *value; + + value = tp_g_value_slice_new (G_TYPE_STRING); + g_value_set_static_string (value, TP_IFACE_CHANNEL_TYPE_TEXT); + g_hash_table_insert (table, (gchar *) text_channel_fixed_properties[0], + value); + + value = tp_g_value_slice_new (G_TYPE_UINT); + g_value_set_uint (value, TP_HANDLE_TYPE_CONTACT); + g_hash_table_insert (table, (gchar *) text_channel_fixed_properties[1], + value); + + func (type, table, text_channel_allowed_properties, user_data); + + g_hash_table_destroy (table); +} + + +static gboolean +rakia_text_manager_requestotron (RakiaTextManager *self, + gpointer request_token, + GHashTable *request_properties, + gboolean require_new) +{ + RakiaTextManagerPrivate *priv = TPSIP_TEXT_MANAGER_GET_PRIVATE (self); + TpBaseConnection *base_conn = (TpBaseConnection *) priv->conn; + TpHandle handle; + GError *error = NULL; + TpExportableChannel *channel; + + if (tp_strdiff (tp_asv_get_string (request_properties, + TP_IFACE_CHANNEL ".ChannelType"), TP_IFACE_CHANNEL_TYPE_TEXT)) + return FALSE; + + if (tp_asv_get_uint32 (request_properties, + TP_IFACE_CHANNEL ".TargetHandleType", NULL) != TP_HANDLE_TYPE_CONTACT) + return FALSE; + + /* validity already checked by TpBaseConnection */ + handle = tp_asv_get_uint32 (request_properties, + TP_IFACE_CHANNEL ".TargetHandle", NULL); + g_assert (handle != 0); + + if (tp_channel_manager_asv_has_unknown_properties (request_properties, + text_channel_fixed_properties, text_channel_allowed_properties, + &error)) + goto error; + + channel = g_hash_table_lookup (priv->channels, + GUINT_TO_POINTER (handle)); + + if (channel == NULL) + { + rakia_text_manager_new_channel (self, + handle, base_conn->self_handle, request_token); + return TRUE; + } + + if (require_new) + { + g_set_error (&error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "Already chatting with contact #%u in another channel", handle); + goto error; + } + + tp_channel_manager_emit_request_already_satisfied (self, request_token, + channel); + return TRUE; + +error: + tp_channel_manager_emit_request_failed (self, request_token, + error->domain, error->code, error->message); + g_error_free (error); + return TRUE; +} + + +static gboolean +rakia_text_manager_create_channel (TpChannelManager *manager, + gpointer request_token, + GHashTable *request_properties) +{ + RakiaTextManager *self = TPSIP_TEXT_MANAGER (manager); + + return rakia_text_manager_requestotron (self, request_token, + request_properties, TRUE); +} + + +static gboolean +rakia_text_manager_request_channel (TpChannelManager *manager, + gpointer request_token, + GHashTable *request_properties) +{ + RakiaTextManager *self = TPSIP_TEXT_MANAGER (manager); + + return rakia_text_manager_requestotron (self, request_token, + request_properties, FALSE); +} + + +static gboolean +rakia_text_manager_ensure_channel (TpChannelManager *manager, + gpointer request_token, + GHashTable *request_properties) +{ + RakiaTextManager *self = TPSIP_TEXT_MANAGER (manager); + + return rakia_text_manager_requestotron (self, request_token, + request_properties, FALSE); +} + +static inline RakiaTextChannel * +rakia_text_manager_lookup_channel (RakiaTextManager *fac, + TpHandle handle) +{ + RakiaTextManagerPrivate *priv = TPSIP_TEXT_MANAGER_GET_PRIVATE (fac); + + return g_hash_table_lookup (priv->channels, + GUINT_TO_POINTER(handle)); +} + +static gboolean +rakia_nua_i_message_cb (TpBaseConnection *conn, + const RakiaNuaEvent *ev, + tagi_t tags[], + RakiaTextManager *fac) +{ + RakiaTextChannel *channel; + TpHandle handle; + const sip_t *sip = ev->sip; + const char *text = ""; + gsize len = 0; + char *allocated_text = NULL; + + /* Block anything else except text/plain messages (like isComposings) */ + if (sip->sip_content_type + && (g_ascii_strcasecmp ("text/plain", sip->sip_content_type->c_type))) + { + nua_respond (ev->nua_handle, + SIP_415_UNSUPPORTED_MEDIA, + SIPTAG_ACCEPT_STR("text/plain"), + NUTAG_WITH_THIS(ev->nua), + TAG_END()); + goto end; + } + + /* If there is some text, assure it's in UTF-8 encoding */ + if (sip->sip_payload && sip->sip_payload->pl_len > 0) + { + const char *charset = NULL; + if (sip->sip_content_type) + { + charset = msg_header_find_param ( + (msg_common_t *) sip->sip_content_type, "charset"); + } + + /* Default charset is UTF-8, we only need to convert if it's a different one */ + if (charset && g_ascii_strcasecmp (charset, "UTF-8")) + { + GError *error; + gsize in_len; + allocated_text = g_convert ( + sip->sip_payload->pl_data, sip->sip_payload->pl_len, + "UTF-8", charset, &in_len, &len, &error); + + if (allocated_text == NULL) + { + gint status; + const char *message = NULL; + + MESSAGE ("character set conversion failed for the message body: %s", error->message); + + if (error->code == G_CONVERT_ERROR_ILLEGAL_SEQUENCE) + { + status = 400; + message = "Invalid character sequence in the message body"; + } + else + { + status = 500; + message = "Character set conversion failed for the message body"; + } + nua_respond (ev->nua_handle, + status, message, + NUTAG_WITH_THIS(ev->nua), + TAG_END()); + + g_error_free (error); + goto end; + } + + text = allocated_text; + + if (in_len != sip->sip_payload->pl_len) + { + nua_respond (ev->nua_handle, + 400, "Incomplete character sequence at the " + "end of the message body", + NUTAG_WITH_THIS(ev->nua), + TAG_END()); + goto end; + } + } + else + { + if (!g_utf8_validate (sip->sip_payload->pl_data, + sip->sip_payload->pl_len, + NULL)) + { + nua_respond (ev->nua_handle, + 400, "Invalid character sequence in the message body", + NUTAG_WITH_THIS(ev->nua), + TAG_END()); + goto end; + } + text = sip->sip_payload->pl_data; + len = (gsize) sip->sip_payload->pl_len; + } + } + + handle = rakia_handle_by_requestor (conn, sip); + + if (!handle) + { + nua_respond (ev->nua_handle, + 400, "Invalid From address", + NUTAG_WITH_THIS(ev->nua), + TAG_END()); + goto end; + } + + /* Send the final response immediately as recommended by RFC 3428 */ + nua_respond (ev->nua_handle, + SIP_200_OK, + NUTAG_WITH_THIS(ev->nua), + TAG_END()); + + DEBUG("Got incoming message from <%s>", + rakia_handle_inspect (conn, handle)); + + channel = rakia_text_manager_lookup_channel (fac, handle); + + if (!channel) + channel = rakia_text_manager_new_channel (fac, + handle, handle, NULL); + + rakia_text_channel_receive (channel, + sip, handle, text, len); + + rakia_handle_unref (conn, handle); + +end: + g_free (allocated_text); + + return TRUE; +} + +static void +connection_status_changed_cb (TpBaseConnection *conn, + guint status, + guint reason, + RakiaTextManager *self) +{ + RakiaTextManagerPrivate *priv = TPSIP_TEXT_MANAGER_GET_PRIVATE (self); + + switch (status) + { + case TP_CONNECTION_STATUS_CONNECTING: + + priv->message_received_id = g_signal_connect (conn, + "nua-event::nua_i_message", + G_CALLBACK (rakia_nua_i_message_cb), + self); + + break; + case TP_CONNECTION_STATUS_DISCONNECTED: + rakia_text_manager_close_all (self); + + if (priv->message_received_id != 0) + { + g_signal_handler_disconnect (conn, priv->message_received_id); + priv->message_received_id = 0; + } + + break; + default: + break; + } +} + +static void +channel_manager_iface_init (gpointer g_iface, gpointer iface_data) +{ + TpChannelManagerIface *iface = g_iface; + + iface->foreach_channel = rakia_text_manager_foreach_channel; + iface->type_foreach_channel_class = + rakia_text_manager_type_foreach_channel_class; + iface->create_channel = rakia_text_manager_create_channel; + iface->request_channel = rakia_text_manager_request_channel; + iface->ensure_channel = rakia_text_manager_ensure_channel; +} diff --git a/rakia/text-manager.h b/rakia/text-manager.h new file mode 100644 index 0000000..fc30edf --- /dev/null +++ b/rakia/text-manager.h @@ -0,0 +1,57 @@ +/* + * rakia/text-manager.h - Text channel manager for SIP + * Copyright (C) 2007 Collabora Ltd. + * Copyright (C) 2007-2011 Nokia Corporation + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __TPSIP_TEXT_MANAGER_H__ +#define __TPSIP_TEXT_MANAGER_H__ + +#include <glib-object.h> + +G_BEGIN_DECLS + +typedef struct _RakiaTextManager RakiaTextManager; +typedef struct _RakiaTextManagerClass RakiaTextManagerClass; + +struct _RakiaTextManagerClass { + GObjectClass parent_class; +}; + +struct _RakiaTextManager { + GObject parent; +}; + +GType rakia_text_manager_get_type(void); + +/* TYPE MACROS */ +#define TPSIP_TYPE_TEXT_MANAGER \ + (rakia_text_manager_get_type()) +#define TPSIP_TEXT_MANAGER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), TPSIP_TYPE_TEXT_MANAGER, RakiaTextManager)) +#define TPSIP_TEXT_MANAGER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), TPSIP_TYPE_TEXT_MANAGER, RakiaTextManagerClass)) +#define TPSIP_IS_TEXT_MANAGER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), TPSIP_TYPE_TEXT_MANAGER)) +#define TPSIP_IS_TEXT_MANAGER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), TPSIP_TYPE_TEXT_MANAGER)) +#define TPSIP_TEXT_MANAGER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), TPSIP_TYPE_TEXT_MANAGER, RakiaTextManagerClass)) + +G_END_DECLS + +#endif diff --git a/rakia/util.c b/rakia/util.c new file mode 100644 index 0000000..164ad1e --- /dev/null +++ b/rakia/util.c @@ -0,0 +1,161 @@ +/* + * util.c - implementation of Telepathy-SofiaSIP utilities + * Copyright (C) 2009, 2010 Nokia Corporation + * @author Mikhail Zabaluev <mikhail.zabaluev@nokia.com> + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include "util.h" + +#include <string.h> + +gchar const * +rakia_version_string (void) +{ + return "Telepathy-SofiaSIP/" TELEPATHY_SIP_VERSION; +} + +static const guchar escape_table[256] = + { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, + /* Control characters except LF and CR. + * NOTE: null character is intentionally flagged */ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 0x10 - 0x1f */ + 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0x22 == '"' */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, /* 0x5c == '\\' */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, /* 0x7f */ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }; + +/** + * rakia_string_append_quoted: + * @buf: a #GString to append the quoted text to + * @text: text to append as a quoted string + * + * Appends to @buf the content of @text as a quoted string accordingly to SIP + * or MIME syntax. + */ +void +rakia_string_append_quoted (GString *buf, const gchar *text) +{ + const gchar *p; + gchar quoted_pair[2] = { '\\', }; + + g_string_append_c (buf, '"'); + + p = text; + while (*p) + { + const gchar *q; + + /* Get the following text span to append verbatim */ + for (q = p; !escape_table[(guchar) *q]; ++q) + { /* do nothing */ } + g_string_append_len (buf, p, q - p); + + if (*q == '\0') + break; + + quoted_pair[1] = *q; + g_string_append_len (buf, quoted_pair, 2); + + p = q + 1; + } + + g_string_append_c (buf, '"'); +} + +/** + * rakia_quote_string: + * @src: the source string + * + * Formats the content of @text as a quoted string accordingly to SIP + * or MIME syntax. + * + * Returns: a newly allocated quoted string. + * The string is to be freed with g_free(). + */ +gchar * +rakia_quote_string (const gchar *src) +{ + GString *buf; + + buf = g_string_sized_new (2); + + rakia_string_append_quoted (buf, src); + + return g_string_free (buf, FALSE); +} + +/** + * rakia_unquote_string: + * @src: the quoted string, including the encompassing double quotes + * @len: length of the string @src in bytes, + * or -1 if the string is null-terminated + * + * Extracts text out of a quoted string literal accordingly to SIP + * or MIME syntax, unescaping characters preceded by backspaces. + * + * Returns: a newly allocated unquoted string. + * The string is to be freed with g_free(). + */ +gchar * +rakia_unquote_string (const gchar *src, gssize len) +{ + gchar *res; + gchar *p; + gsize i; + + g_return_val_if_fail (src != NULL, NULL); + + if (len < 0) + len = strlen (src); + + g_return_val_if_fail (len >= 2, NULL); + g_return_val_if_fail (src[0] == '"' && src[len - 1] == '"', NULL); + + res = g_malloc (len - 2 + 1); + + p = res; + + for (i = 1; i < len - 1; ++i) + { + if (src[i] == '\\') + { + ++i; + if (G_UNLIKELY (i == len - 1)) + { + g_critical ("the quoted string is missing the termination quote, improperly escaped?"); + return NULL; + } + } + *p++ = src[i]; + } + + *p = '\0'; + + return res; +} diff --git a/rakia/util.h b/rakia/util.h new file mode 100644 index 0000000..c4baef4 --- /dev/null +++ b/rakia/util.h @@ -0,0 +1,38 @@ +/* + * util.h - declarations for Telepathy-SofiaSIP utilities + * Copyright (C) 2009 Nokia Corporation + * @author Mikhail Zabaluev <mikhail.zabaluev@nokia.com> + * + * This work 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 work 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 work; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef TPSIP_UTIL_H_ +#define TPSIP_UTIL_H_ + +#include <glib.h> + +G_BEGIN_DECLS + +gchar * rakia_quote_string (const gchar *src); + +gchar * rakia_unquote_string (const gchar *src, gssize len); + +void rakia_string_append_quoted (GString *buf, const gchar *text); + +gchar const *rakia_version_string (); + +G_END_DECLS + +#endif /* !TPSIP_UTIL_H_ */ |