summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/Makefile.am12
-rw-r--r--src/call-member.c2
-rw-r--r--src/call-muc-channel.c2
-rw-r--r--src/gtalk-file-collection.c8
-rw-r--r--src/jingle-content.c33
-rw-r--r--src/jingle-content.h3
-rw-r--r--src/jingle-factory.c3
-rw-r--r--src/jingle-factory.h36
-rw-r--r--src/jingle-session.c244
-rw-r--r--src/jingle-session.h4
-rw-r--r--src/media-channel.c210
-rw-r--r--src/protocol.c12
-rw-r--r--tests/twisted/Makefile.am2
-rw-r--r--tests/twisted/constants.py9
-rw-r--r--tests/twisted/jingle/incoming-call-stream-error.py61
-rw-r--r--tests/twisted/jingle/stream-errors-on-content-reject.py240
-rw-r--r--tests/twisted/jingle/stream-errors-on-terminate.py134
-rw-r--r--tests/twisted/jingle/test-outgoing-call-rejected.py21
-rw-r--r--tools/telepathy.am13
19 files changed, 866 insertions, 183 deletions
diff --git a/src/Makefile.am b/src/Makefile.am
index 7217a0b6a..b8bdf00af 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -190,6 +190,10 @@ libgabble_convenience_la_SOURCES = \
vcard-manager.h \
vcard-manager.c
+enumtype_sources = \
+ jingle-factory.h \
+ presence.h
+
libgabble_convenience_la_LIBADD = \
$(top_builddir)/extensions/libgabble-extensions.la \
$(top_builddir)/lib/gibber/libgibber.la \
@@ -257,19 +261,19 @@ gabble-signals-marshal.list: $(libgabble_convenience_la_SOURCES) Makefile.am
} > $@
# rules for making the glib enum objects
-gabble-enumtypes.h: presence.h Makefile.in
+gabble-enumtypes.h: $(enumtype_sources) Makefile.in
$(AM_V_GEN)glib-mkenums \
--fhead "#ifndef __GABBLE_ENUM_TYPES_H__\n#define __GABBLE_ENUM_TYPES_H__\n\n#include <glib-object.h>\n\nG_BEGIN_DECLS\n" \
--fprod "/* enumerations from \"@filename@\" */\n" \
--vhead "GType @enum_name@_get_type (void);\n#define GABBLE_TYPE_@ENUMSHORT@ (@enum_name@_get_type())\n" \
--ftail "G_END_DECLS\n\n#endif /* __GABBLE_ENUM_TYPES_H__ */" \
- $< > $@
+ $(enumtype_sources) > $@
-gabble-enumtypes.c: presence.h Makefile.in
+gabble-enumtypes.c: $(enumtype_sources) Makefile.in
$(AM_V_GEN)glib-mkenums \
--fhead "#include <$*.h>" \
--fprod "\n/* enumerations from \"@filename@\" */\n#include \"@filename@\"" \
--vhead "GType\n@enum_name@_get_type (void)\n{\n static GType etype = 0;\n if (etype == 0) {\n static const G@Type@Value values[] = {" \
--vprod " { @VALUENAME@, \"@VALUENAME@\", \"@valuenick@\" }," \
--vtail " { 0, NULL, NULL }\n };\n etype = g_@type@_register_static (\"@EnumName@\", values);\n }\n return etype;\n}\n" \
- $< > $@
+ $(enumtype_sources) > $@
diff --git a/src/call-member.c b/src/call-member.c
index 33cda7cdd..beb73d139 100644
--- a/src/call-member.c
+++ b/src/call-member.c
@@ -592,7 +592,7 @@ gabble_call_member_shutdown (GabbleCallMember *self)
if (priv->session != NULL)
{
gabble_jingle_session_terminate (priv->session,
- TP_CHANNEL_GROUP_CHANGE_REASON_NONE, NULL, NULL);
+ JINGLE_REASON_UNKNOWN, NULL, NULL);
}
/* removing the content will remove it from our list */
diff --git a/src/call-muc-channel.c b/src/call-muc-channel.c
index bd888d9e1..09c588af5 100644
--- a/src/call-muc-channel.c
+++ b/src/call-muc-channel.c
@@ -1062,7 +1062,7 @@ gabble_call_muc_channel_incoming_session (GabbleCallMucChannel *self,
if (member == NULL || gabble_call_member_get_session (member) != NULL)
{
gabble_jingle_session_terminate (session,
- TP_CHANNEL_GROUP_CHANGE_REASON_NONE,
+ JINGLE_REASON_UNKNOWN,
"Muji jingle session initiated while there already was one",
NULL);
}
diff --git a/src/gtalk-file-collection.c b/src/gtalk-file-collection.c
index 57bcd3f17..21a3dd128 100644
--- a/src/gtalk-file-collection.c
+++ b/src/gtalk-file-collection.c
@@ -242,7 +242,7 @@ gtalk_file_collection_dispose (GObject *object)
if (self->priv->jingle != NULL)
gabble_jingle_session_terminate (self->priv->jingle,
- TP_CHANNEL_GROUP_CHANGE_REASON_NONE, NULL, NULL);
+ JINGLE_REASON_UNKNOWN, NULL, NULL);
tp_clear_object (&self->priv->jingle);
@@ -464,7 +464,7 @@ jingle_session_state_changed_cb (GabbleJingleSession *session,
static void
jingle_session_terminated_cb (GabbleJingleSession *session,
gboolean local_terminator,
- TpChannelGroupChangeReason reason,
+ JingleReason reason,
const gchar *text,
gpointer user_data)
{
@@ -1714,7 +1714,7 @@ gtalk_file_collection_terminate (GTalkFileCollection *self,
jingle session */
self->priv->status = GTALK_FT_STATUS_TERMINATED;
gabble_jingle_session_terminate (self->priv->jingle,
- TP_CHANNEL_GROUP_CHANGE_REASON_NONE, NULL, NULL);
+ JINGLE_REASON_UNKNOWN, NULL, NULL);
return;
}
return;
@@ -1755,7 +1755,7 @@ channel_disposed (gpointer data, GObject *object)
jingle session */
self->priv->status = GTALK_FT_STATUS_TERMINATED;
gabble_jingle_session_terminate (self->priv->jingle,
- TP_CHANNEL_GROUP_CHANGE_REASON_NONE, NULL, NULL);
+ JINGLE_REASON_UNKNOWN, NULL, NULL);
return;
}
}
diff --git a/src/jingle-content.c b/src/jingle-content.c
index 9d5a80a2f..1db1c3c52 100644
--- a/src/jingle-content.c
+++ b/src/jingle-content.c
@@ -1167,8 +1167,10 @@ _on_remove_reply (GObject *c_as_obj,
g_signal_emit (c, signals[REMOVED], 0);
}
-void
-gabble_jingle_content_remove (GabbleJingleContent *c, gboolean signal_peer)
+static void
+_content_remove (GabbleJingleContent *c,
+ gboolean signal_peer,
+ JingleReason reason)
{
GabbleJingleContentPrivate *priv = c->priv;
LmMessage *msg;
@@ -1191,7 +1193,18 @@ gabble_jingle_content_remove (GabbleJingleContent *c, gboolean signal_peer)
g_object_notify ((GObject *) c, "state");
msg = gabble_jingle_session_new_message (c->session,
- JINGLE_ACTION_CONTENT_REMOVE, &sess_node);
+ reason == JINGLE_REASON_UNKNOWN ?
+ JINGLE_ACTION_CONTENT_REMOVE : JINGLE_ACTION_CONTENT_REJECT,
+ &sess_node);
+
+ if (reason != JINGLE_REASON_UNKNOWN)
+ {
+ LmMessageNode *reason_node = lm_message_node_add_child (sess_node,
+ "reason", NULL);
+ lm_message_node_add_child (reason_node,
+ gabble_jingle_session_get_reason_name (reason), NULL);
+ }
+
gabble_jingle_content_produce_node (c, sess_node, FALSE, FALSE, NULL);
gabble_jingle_session_send (c->session, msg, _on_remove_reply,
(GObject *) c);
@@ -1206,6 +1219,20 @@ gabble_jingle_content_remove (GabbleJingleContent *c, gboolean signal_peer)
}
}
+void
+gabble_jingle_content_remove (GabbleJingleContent *c,
+ gboolean signal_peer)
+{
+ _content_remove (c, signal_peer, JINGLE_REASON_UNKNOWN);
+}
+
+void
+gabble_jingle_content_reject (GabbleJingleContent *c,
+ JingleReason reason)
+{
+ _content_remove (c, TRUE, reason);
+}
+
gboolean
gabble_jingle_content_is_created_by_us (GabbleJingleContent *c)
{
diff --git a/src/jingle-content.h b/src/jingle-content.h
index 4f04d551b..9305ff638 100644
--- a/src/jingle-content.h
+++ b/src/jingle-content.h
@@ -126,6 +126,9 @@ gboolean gabble_jingle_content_is_ready (GabbleJingleContent *self);
void gabble_jingle_content_set_transport_state (GabbleJingleContent *content,
JingleTransportState state);
void gabble_jingle_content_remove (GabbleJingleContent *c, gboolean signal_peer);
+void gabble_jingle_content_reject (GabbleJingleContent *c,
+ JingleReason reason);
+
GList *gabble_jingle_content_get_remote_candidates (GabbleJingleContent *c);
GList *gabble_jingle_content_get_local_candidates (GabbleJingleContent *c);
gboolean gabble_jingle_content_change_direction (GabbleJingleContent *c,
diff --git a/src/jingle-factory.c b/src/jingle-factory.c
index 9b5fb7f5a..13075e4c2 100644
--- a/src/jingle-factory.c
+++ b/src/jingle-factory.c
@@ -810,8 +810,7 @@ REQUEST_ERROR:
g_error_free (error);
if (sess != NULL && new_session)
- gabble_jingle_session_terminate (sess, TP_CHANNEL_GROUP_CHANGE_REASON_NONE,
- NULL, NULL);
+ gabble_jingle_session_terminate (sess, JINGLE_REASON_UNKNOWN, NULL, NULL);
return LM_HANDLER_RESULT_REMOVE_MESSAGE;
}
diff --git a/src/jingle-factory.h b/src/jingle-factory.h
index ac6c4be4e..62563057d 100644
--- a/src/jingle-factory.h
+++ b/src/jingle-factory.h
@@ -26,7 +26,7 @@
G_BEGIN_DECLS
-typedef enum {
+typedef enum { /*< skip >*/
/* not a jingle message */
JINGLE_DIALECT_ERROR,
/* old libjingle3 gtalk variant */
@@ -42,7 +42,7 @@ typedef enum {
#define JINGLE_IS_GOOGLE_DIALECT(d)\
((d == JINGLE_DIALECT_GTALK3) || (d == JINGLE_DIALECT_GTALK4))
-typedef enum {
+typedef enum { /*< skip >*/
JINGLE_STATE_INVALID = -1,
JINGLE_STATE_PENDING_CREATED = 0,
JINGLE_STATE_PENDING_INITIATE_SENT,
@@ -53,7 +53,7 @@ typedef enum {
MAX_JINGLE_STATES
} JingleState;
-typedef enum {
+typedef enum { /*< skip >*/
JINGLE_ACTION_UNKNOWN,
JINGLE_ACTION_CONTENT_ACCEPT,
JINGLE_ACTION_CONTENT_ADD,
@@ -71,31 +71,53 @@ typedef enum {
JINGLE_ACTION_INFO
} JingleAction;
-typedef enum {
+typedef enum { /*< skip >*/
JINGLE_CONTENT_SENDERS_NONE,
JINGLE_CONTENT_SENDERS_INITIATOR,
JINGLE_CONTENT_SENDERS_RESPONDER,
JINGLE_CONTENT_SENDERS_BOTH
} JingleContentSenders;
-typedef enum {
+typedef enum { /*< skip >*/
JINGLE_TRANSPORT_UNKNOWN,
JINGLE_TRANSPORT_GOOGLE_P2P,
JINGLE_TRANSPORT_RAW_UDP,
JINGLE_TRANSPORT_ICE_UDP,
} JingleTransportType;
-typedef enum {
+typedef enum { /*< skip >*/
JINGLE_TRANSPORT_PROTOCOL_UDP,
JINGLE_TRANSPORT_PROTOCOL_TCP
} JingleTransportProtocol;
-typedef enum {
+typedef enum { /*< skip >*/
JINGLE_CANDIDATE_TYPE_LOCAL,
JINGLE_CANDIDATE_TYPE_STUN,
JINGLE_CANDIDATE_TYPE_RELAY
} JingleCandidateType;
+typedef enum
+{
+ JINGLE_REASON_UNKNOWN,
+ JINGLE_REASON_ALTERNATIVE_SESSION,
+ JINGLE_REASON_BUSY,
+ JINGLE_REASON_CANCEL,
+ JINGLE_REASON_CONNECTIVITY_ERROR,
+ JINGLE_REASON_DECLINE,
+ JINGLE_REASON_EXPIRED,
+ JINGLE_REASON_FAILED_APPLICATION,
+ JINGLE_REASON_FAILED_TRANSPORT,
+ JINGLE_REASON_GENERAL_ERROR,
+ JINGLE_REASON_GONE,
+ JINGLE_REASON_INCOMPATIBLE_PARAMETERS,
+ JINGLE_REASON_MEDIA_ERROR,
+ JINGLE_REASON_SECURITY_ERROR,
+ JINGLE_REASON_SUCCESS,
+ JINGLE_REASON_TIMEOUT,
+ JINGLE_REASON_UNSUPPORTED_APPLICATIONS,
+ JINGLE_REASON_UNSUPPORTED_TRANSPORTS
+} JingleReason;
+
typedef struct _GabbleJingleFactoryClass GabbleJingleFactoryClass;
GType gabble_jingle_factory_get_type (void);
diff --git a/src/jingle-session.c b/src/jingle-session.c
index 97e81aaa3..b4701fccc 100644
--- a/src/jingle-session.c
+++ b/src/jingle-session.c
@@ -26,6 +26,7 @@
#include <loudmouth/loudmouth.h>
#include <telepathy-glib/handle-repo-dynamic.h>
+#include <wocky/wocky-utils.h>
#define DEBUG_FLAG GABBLE_DEBUG_MEDIA
@@ -34,6 +35,7 @@
#include "conn-presence.h"
#include "debug.h"
#include "gabble-signals-marshal.h"
+#include "gabble-enumtypes.h"
#include "jingle-content.h"
#include "jingle-factory.h"
/* FIXME: the RTP-specific bits of this file should be separated from the
@@ -52,6 +54,7 @@ enum
NEW_CONTENT,
REMOTE_STATE_CHANGED,
TERMINATED,
+ CONTENT_REJECTED,
LAST_SIGNAL
};
@@ -479,12 +482,45 @@ gabble_jingle_session_class_init (GabbleJingleSessionClass *cls)
G_TYPE_FROM_CLASS (cls), G_SIGNAL_RUN_LAST,
0, NULL, NULL, g_cclosure_marshal_VOID__VOID,
G_TYPE_NONE, 0);
+ signals[CONTENT_REJECTED] = g_signal_new ("content-rejected",
+ G_TYPE_FROM_CLASS (cls), G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, gabble_marshal_VOID__OBJECT_UINT_STRING,
+ G_TYPE_NONE, 3, G_TYPE_OBJECT, G_TYPE_UINT, G_TYPE_STRING);
}
typedef void (*HandlerFunc)(GabbleJingleSession *sess,
- LmMessageNode *node, GError **error);
+ LmMessageNode *node, GError **error);
typedef void (*ContentHandlerFunc)(GabbleJingleSession *sess,
- GabbleJingleContent *c, LmMessageNode *content_node, GError **error);
+ GabbleJingleContent *c, LmMessageNode *content_node, gpointer user_data,
+ GError **error);
+
+static gboolean
+extract_reason (WockyNode *node, JingleReason *reason, gchar **message)
+{
+ JingleReason _reason = JINGLE_REASON_UNKNOWN;
+ WockyNode *child;
+ WockyNodeIter iter;
+
+ g_return_val_if_fail (node != NULL, FALSE);
+
+ if (message != NULL)
+ *message = g_strdup (wocky_node_get_content_from_child (node, "text"));
+
+ wocky_node_iter_init (&iter, node, NULL, NULL);
+
+ while (wocky_node_iter_next (&iter, &child))
+ {
+ if (wocky_enum_from_nick (
+ jingle_reason_get_type (), child->name, (gint *) &_reason))
+ {
+ if (reason != NULL)
+ *reason = _reason;
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
static JingleAction
parse_action (const gchar *txt)
@@ -592,9 +628,7 @@ action_is_allowed (JingleAction action, JingleState state)
static void gabble_jingle_session_send_rtp_info (GabbleJingleSession *sess,
const gchar *name);
static void set_state (GabbleJingleSession *sess,
- JingleState state,
- TpChannelGroupChangeReason termination_reason,
- const gchar *text);
+ JingleState state, JingleReason termination_reason, const gchar *text);
static GabbleJingleContent *_get_any_content (GabbleJingleSession *session);
static gboolean
@@ -682,6 +716,7 @@ _foreach_content (GabbleJingleSession *sess,
LmMessageNode *node,
gboolean fail_if_missing,
ContentHandlerFunc func,
+ gpointer user_data,
GError **error)
{
GabbleJingleContent *c;
@@ -700,7 +735,7 @@ _foreach_content (GabbleJingleSession *sess,
fail_if_missing, &c, error))
return;
- func (sess, c, content_node, error);
+ func (sess, c, content_node, user_data, error);
if (*error != NULL)
return;
}
@@ -829,7 +864,7 @@ create_content (GabbleJingleSession *sess, GType content_type,
static void
_each_content_add (GabbleJingleSession *sess, GabbleJingleContent *c,
- LmMessageNode *content_node, GError **error)
+ LmMessageNode *content_node, gpointer user_data, GError **error)
{
GabbleJingleSessionPrivate *priv = sess->priv;
const gchar *name = lm_message_node_get_attribute (content_node, "name");
@@ -873,7 +908,7 @@ _each_content_add (GabbleJingleSession *sess, GabbleJingleContent *c,
static void
_each_content_remove (GabbleJingleSession *sess, GabbleJingleContent *c,
- LmMessageNode *content_node, GError **error)
+ LmMessageNode *content_node, gpointer user_data, GError **error)
{
g_assert (c != NULL);
@@ -881,8 +916,20 @@ _each_content_remove (GabbleJingleSession *sess, GabbleJingleContent *c,
}
static void
+_each_content_rejected (GabbleJingleSession *sess, GabbleJingleContent *c,
+ LmMessageNode *content_node, gpointer user_data, GError **error)
+{
+ JingleReason reason = GPOINTER_TO_UINT (user_data);
+ g_assert (c != NULL);
+
+ g_signal_emit (sess, signals[CONTENT_REJECTED], 0, c, reason, "");
+
+ gabble_jingle_content_remove (c, FALSE);
+}
+
+static void
_each_content_modify (GabbleJingleSession *sess, GabbleJingleContent *c,
- LmMessageNode *content_node, GError **error)
+ LmMessageNode *content_node, gpointer user_data, GError **error)
{
g_assert (c != NULL);
@@ -894,19 +941,19 @@ _each_content_modify (GabbleJingleSession *sess, GabbleJingleContent *c,
static void
_each_content_replace (GabbleJingleSession *sess, GabbleJingleContent *c,
- LmMessageNode *content_node, GError **error)
+ LmMessageNode *content_node, gpointer user_data, GError **error)
{
- _each_content_remove (sess, c, content_node, error);
+ _each_content_remove (sess, c, content_node, NULL, error);
if (*error != NULL)
return;
- _each_content_add (sess, c, content_node, error);
+ _each_content_add (sess, c, content_node, NULL, error);
}
static void
_each_content_accept (GabbleJingleSession *sess, GabbleJingleContent *c,
- LmMessageNode *content_node ,GError **error)
+ LmMessageNode *content_node, gpointer user_data, GError **error)
{
GabbleJingleSessionPrivate *priv = sess->priv;
JingleContentState state;
@@ -929,7 +976,7 @@ _each_content_accept (GabbleJingleSession *sess, GabbleJingleContent *c,
static void
_each_description_info (GabbleJingleSession *sess, GabbleJingleContent *c,
- LmMessageNode *content_node, GError **error)
+ LmMessageNode *content_node, gpointer user_data, GError **error)
{
gabble_jingle_content_parse_description_info (c, content_node, error);
}
@@ -945,8 +992,7 @@ on_session_initiate (GabbleJingleSession *sess, LmMessageNode *node,
{
/* We ignore initiate from us, and terminate the session immediately
* afterwards */
- gabble_jingle_session_terminate (sess,
- TP_CHANNEL_GROUP_CHANGE_REASON_BUSY, NULL, NULL);
+ gabble_jingle_session_terminate (sess, JINGLE_REASON_BUSY, NULL, NULL);
return;
}
@@ -976,17 +1022,17 @@ on_session_initiate (GabbleJingleSession *sess, LmMessageNode *node,
}
else
{
- _each_content_add (sess, NULL, node, error);
+ _each_content_add (sess, NULL, node, NULL, error);
}
}
else if (priv->dialect == JINGLE_DIALECT_GTALK4)
{
/* in this case we implicitly have just one content */
- _each_content_add (sess, NULL, node, error);
+ _each_content_add (sess, NULL, node, NULL, error);
}
else
{
- _foreach_content (sess, node, FALSE, _each_content_add, error);
+ _foreach_content (sess, node, FALSE, _each_content_add, NULL, error);
}
if (*error == NULL)
@@ -995,7 +1041,8 @@ on_session_initiate (GabbleJingleSession *sess, LmMessageNode *node,
* disposition; resolve this as soon as the proper procedure is defined
* in XEP-0166. */
- set_state (sess, JINGLE_STATE_PENDING_INITIATED, 0, NULL);
+ set_state (sess, JINGLE_STATE_PENDING_INITIATED, JINGLE_REASON_UNKNOWN,
+ NULL);
gabble_jingle_session_send_rtp_info (sess, "ringing");
}
@@ -1005,45 +1052,54 @@ static void
on_content_add (GabbleJingleSession *sess, LmMessageNode *node,
GError **error)
{
- _foreach_content (sess, node, FALSE, _each_content_add, error);
+ _foreach_content (sess, node, FALSE, _each_content_add, NULL, error);
}
static void
on_content_modify (GabbleJingleSession *sess, LmMessageNode *node,
GError **error)
{
- _foreach_content (sess, node, TRUE, _each_content_modify, error);
+ _foreach_content (sess, node, TRUE, _each_content_modify, NULL, error);
}
static void
on_content_remove (GabbleJingleSession *sess, LmMessageNode *node,
GError **error)
{
- _foreach_content (sess, node, TRUE, _each_content_remove, error);
+ _foreach_content (sess, node, TRUE, _each_content_remove, NULL, error);
}
static void
on_content_replace (GabbleJingleSession *sess, LmMessageNode *node,
GError **error)
{
- _foreach_content (sess, node, TRUE, _each_content_replace, error);
+ _foreach_content (sess, node, TRUE, _each_content_replace, NULL, error);
}
static void
on_content_reject (GabbleJingleSession *sess, LmMessageNode *node,
GError **error)
{
- /* FIXME: reject is different from remove - remove is for
- * acknowledged contents, reject is for pending; but the result
- * is the same. */
- _foreach_content (sess, node, TRUE, _each_content_remove, error);
+ LmMessageNode *n = lm_message_node_get_child (node, "reason");
+ JingleReason reason = JINGLE_REASON_UNKNOWN;
+
+ DEBUG (" ");
+
+ if (n != NULL)
+ extract_reason (n, &reason, NULL);
+
+ if (reason == JINGLE_REASON_UNKNOWN)
+ reason = JINGLE_REASON_GENERAL_ERROR;
+
+ _foreach_content (sess, node, TRUE, _each_content_rejected,
+ GUINT_TO_POINTER (reason), error);
}
static void
on_content_accept (GabbleJingleSession *sess, LmMessageNode *node,
GError **error)
{
- _foreach_content (sess, node, TRUE, _each_content_accept, error);
+ _foreach_content (sess, node, TRUE, _each_content_accept, NULL, error);
}
static void
@@ -1066,19 +1122,19 @@ on_session_accept (GabbleJingleSession *sess, LmMessageNode *node,
GList *l;
for (l = cs; l != NULL; l = l->next)
- _each_content_accept (sess, l->data, node, error);
+ _each_content_accept (sess, l->data, node, NULL, error);
g_list_free (cs);
}
else
{
- _foreach_content (sess, node, TRUE, _each_content_accept, error);
+ _foreach_content (sess, node, TRUE, _each_content_accept, NULL, error);
}
if (*error != NULL)
return;
- set_state (sess, JINGLE_STATE_ACTIVE, 0, NULL);
+ set_state (sess, JINGLE_STATE_ACTIVE, JINGLE_REASON_UNKNOWN, NULL);
if (priv->dialect != JINGLE_DIALECT_V032)
{
@@ -1259,76 +1315,24 @@ on_session_info (GabbleJingleSession *sess,
"no recognized session-info payloads");
}
-typedef struct {
- const gchar *element;
- TpChannelGroupChangeReason reason;
-} ReasonMapping;
-
-/* Taken from the schema in XEP 0166 */
-ReasonMapping reasons[] = {
- { "alternative-session", TP_CHANNEL_GROUP_CHANGE_REASON_NONE },
- { "busy", TP_CHANNEL_GROUP_CHANGE_REASON_BUSY },
- { "cancel", TP_CHANNEL_GROUP_CHANGE_REASON_NONE },
- { "connectivity-error", TP_CHANNEL_GROUP_CHANGE_REASON_ERROR },
- { "decline", TP_CHANNEL_GROUP_CHANGE_REASON_NONE },
- { "expired", TP_CHANNEL_GROUP_CHANGE_REASON_NONE },
- { "failed-application", TP_CHANNEL_GROUP_CHANGE_REASON_ERROR },
- { "failed-transport", TP_CHANNEL_GROUP_CHANGE_REASON_ERROR },
- { "general-error", TP_CHANNEL_GROUP_CHANGE_REASON_ERROR },
- { "gone", TP_CHANNEL_GROUP_CHANGE_REASON_OFFLINE },
- { "incompatible-parameters", TP_CHANNEL_GROUP_CHANGE_REASON_ERROR },
- { "media-error", TP_CHANNEL_GROUP_CHANGE_REASON_ERROR },
- { "security-error", TP_CHANNEL_GROUP_CHANGE_REASON_ERROR },
- { "success", TP_CHANNEL_GROUP_CHANGE_REASON_NONE },
- { "timeout", TP_CHANNEL_GROUP_CHANGE_REASON_NO_ANSWER },
- { "unsupported-applications", TP_CHANNEL_GROUP_CHANGE_REASON_ERROR },
- { "unsupported-transports", TP_CHANNEL_GROUP_CHANGE_REASON_ERROR },
- { NULL, }
-};
-
static void
on_session_terminate (GabbleJingleSession *sess, LmMessageNode *node,
GError **error)
{
- TpChannelGroupChangeReason reason = TP_CHANNEL_GROUP_CHANGE_REASON_NONE;
- const gchar *text = NULL;
+ gchar *text = NULL;
LmMessageNode *n = lm_message_node_get_child (node, "reason");
- ReasonMapping *m = NULL;
- NodeIter i;
+ JingleReason jingle_reason = JINGLE_REASON_UNKNOWN;
- /* If the session-terminate stanza has a <reason> child, then iterate across
- * its children, looking for a child whose name we recognise as a
- * machine-readable reason for the call ending (looked up from the table
- * above), and a <text> node containing a human-readable message.
- */
if (n != NULL)
- for (i = node_iter (n); i; i = node_iter_next (i))
- {
- const gchar *name;
+ extract_reason (n, &jingle_reason, &text);
- n = node_iter_data (i);
-
- name = lm_message_node_get_name (n);
+ DEBUG ("remote end terminated the session with reason %s and text '%s'",
+ gabble_jingle_session_get_reason_name (jingle_reason),
+ (text != NULL ? text : "(none)"));
- if (!tp_strdiff (name, "text"))
- {
- text = lm_message_node_get_value (n);
- continue;
- }
+ set_state (sess, JINGLE_STATE_ENDED, jingle_reason, text);
- for (m = reasons; m->element != NULL; m++)
- if (!tp_strdiff (m->element, name))
- {
- reason = m->reason;
- break;
- }
- }
-
- DEBUG ("remote end terminated the session with reason %s (%u) "
- "and text '%s'",
- (m != NULL && m->element != NULL ? m->element : "(none)"), reason,
- (text != NULL ? text : "(none)"));
- set_state (sess, JINGLE_STATE_ENDED, reason, text);
+ g_free (text);
}
static void
@@ -1409,7 +1413,7 @@ static void
on_description_info (GabbleJingleSession *sess, LmMessageNode *node,
GError **error)
{
- _foreach_content (sess, node, TRUE, _each_description_info, error);
+ _foreach_content (sess, node, TRUE, _each_description_info, NULL, error);
}
static void
@@ -1816,7 +1820,7 @@ _on_initiate_reply (GObject *sess_as_obj,
}
else
{
- set_state (sess, JINGLE_STATE_ENDED, TP_CHANNEL_GROUP_CHANGE_REASON_NONE,
+ set_state (sess, JINGLE_STATE_ENDED, JINGLE_REASON_UNKNOWN,
NULL);
}
}
@@ -1835,7 +1839,7 @@ _on_accept_reply (GObject *sess_as_obj,
}
else
{
- set_state (sess, JINGLE_STATE_ENDED, TP_CHANNEL_GROUP_CHANGE_REASON_NONE,
+ set_state (sess, JINGLE_STATE_ENDED, JINGLE_REASON_UNKNOWN,
NULL);
}
}
@@ -1966,14 +1970,14 @@ try_session_initiate_or_accept (GabbleJingleSession *sess)
* @sess: a jingle session
* @state: the new state for the session
* @termination_reason: if @state is JINGLE_STATE_ENDED, the reason the session
- * ended. Otherwise, must be 0.
+ * ended. Otherwise, must be JINGLE_REASON_UNKNOWN.
* @text: if @state is JINGLE_STATE_ENDED, the human-readable reason the session
* ended.
*/
static void
set_state (GabbleJingleSession *sess,
JingleState state,
- TpChannelGroupChangeReason termination_reason,
+ JingleReason termination_reason,
const gchar *text)
{
GabbleJingleSessionPrivate *priv = sess->priv;
@@ -1985,7 +1989,7 @@ set_state (GabbleJingleSession *sess,
}
if (state != JINGLE_STATE_ENDED)
- g_assert (termination_reason == 0);
+ g_assert (termination_reason == JINGLE_REASON_UNKNOWN);
DEBUG ("Setting state of JingleSession: %p (priv = %p) from %u to %u", sess, priv, priv->state, state);
@@ -2013,33 +2017,20 @@ gabble_jingle_session_accept (GabbleJingleSession *sess)
try_session_initiate_or_accept (sess);
}
-static const gchar *
-_get_jingle_reason (GabbleJingleSession *sess,
- TpChannelGroupChangeReason reason)
+const gchar *
+gabble_jingle_session_get_reason_name (JingleReason reason)
{
- switch (reason)
- {
- case TP_CHANNEL_GROUP_CHANGE_REASON_NONE:
- if (sess->priv->state == JINGLE_STATE_ACTIVE)
- return "success";
- else
- return "cancel";
- case TP_CHANNEL_GROUP_CHANGE_REASON_OFFLINE:
- return "gone";
- case TP_CHANNEL_GROUP_CHANGE_REASON_BUSY:
- return "busy";
- case TP_CHANNEL_GROUP_CHANGE_REASON_ERROR:
- return "general-error";
- case TP_CHANNEL_GROUP_CHANGE_REASON_NO_ANSWER:
- return "timeout";
- default:
- return NULL;
- }
+ GEnumClass *klass = g_type_class_ref (jingle_reason_get_type ());
+ GEnumValue *enum_value = g_enum_get_value (klass, (gint) reason);
+
+ g_return_val_if_fail (enum_value != NULL, NULL);
+
+ return enum_value->value_nick;
}
gboolean
gabble_jingle_session_terminate (GabbleJingleSession *sess,
- TpChannelGroupChangeReason reason,
+ JingleReason reason,
const gchar *text,
GError **error)
{
@@ -2052,14 +2043,11 @@ gabble_jingle_session_terminate (GabbleJingleSession *sess,
return TRUE;
}
- reason_elt = _get_jingle_reason (sess, reason);
+ if (reason == JINGLE_REASON_UNKNOWN)
+ reason = (priv->state == JINGLE_STATE_ACTIVE) ?
+ JINGLE_REASON_SUCCESS : JINGLE_REASON_CANCEL;
- if (reason_elt == NULL)
- {
- g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT,
- "%u doesn't make sense as a reason to end a call", reason);
- return FALSE;
- }
+ reason_elt = gabble_jingle_session_get_reason_name (reason);
if (priv->state != JINGLE_STATE_PENDING_CREATED)
{
@@ -2139,7 +2127,7 @@ content_removed_cb (GabbleJingleContent *c, gpointer user_data)
if (count_active_contents (sess) == 0)
{
gabble_jingle_session_terminate (sess,
- TP_CHANNEL_GROUP_CHANGE_REASON_NONE, NULL, NULL);
+ JINGLE_REASON_UNKNOWN, NULL, NULL);
}
else
{
diff --git a/src/jingle-session.h b/src/jingle-session.h
index 446f66fde..f2a1fb83a 100644
--- a/src/jingle-session.h
+++ b/src/jingle-session.h
@@ -84,7 +84,7 @@ LmMessage *gabble_jingle_session_new_message (GabbleJingleSession *sess,
void gabble_jingle_session_accept (GabbleJingleSession *sess);
gboolean gabble_jingle_session_terminate (GabbleJingleSession *sess,
- TpChannelGroupChangeReason reason,
+ JingleReason reason,
const gchar *text,
GError **error);
void gabble_jingle_session_remove_content (GabbleJingleSession *sess,
@@ -127,5 +127,7 @@ gboolean gabble_jingle_session_defines_action (GabbleJingleSession *sess,
const gchar *gabble_jingle_session_get_peer_jid (GabbleJingleSession *sess);
+const gchar *gabble_jingle_session_get_reason_name (JingleReason reason);
+
#endif /* __JINGLE_SESSION_H__ */
diff --git a/src/media-channel.c b/src/media-channel.c
index 57f27681f..eef53ea5d 100644
--- a/src/media-channel.c
+++ b/src/media-channel.c
@@ -212,9 +212,7 @@ gabble_media_channel_init (GabbleMediaChannel *self)
static void session_state_changed_cb (GabbleJingleSession *session,
GParamSpec *arg1, GabbleMediaChannel *channel);
static void session_terminated_cb (GabbleJingleSession *session,
- gboolean local_terminator,
- TpChannelGroupChangeReason reason,
- const gchar *text,
+ gboolean local_terminator, JingleReason reason, const gchar *text,
gpointer user_data);
static void session_new_content_cb (GabbleJingleSession *session,
GabbleJingleContent *c, gpointer user_data);
@@ -223,6 +221,9 @@ static void create_stream_from_content (GabbleMediaChannel *chan,
static gboolean contact_is_media_capable (GabbleMediaChannel *chan, TpHandle peer,
gboolean *wait, GError **error);
static void stream_creation_data_cancel (gpointer p, gpointer unused);
+static void session_content_rejected_cb (GabbleJingleSession *session,
+ GabbleJingleContent *c, JingleReason reason, const gchar *message,
+ gpointer user_data);
static void
create_initial_streams (GabbleMediaChannel *chan)
@@ -289,6 +290,9 @@ _latch_to_session (GabbleMediaChannel *chan)
g_signal_connect (priv->session, "terminated",
(GCallback) session_terminated_cb, chan);
+ g_signal_connect (priv->session, "content-rejected",
+ (GCallback) session_content_rejected_cb, chan);
+
gabble_media_channel_hold_latch_to_session (chan);
g_assert (priv->streams->len == 0);
@@ -1055,7 +1059,7 @@ gabble_media_channel_close (GabbleMediaChannel *self)
if (priv->session != NULL)
gabble_jingle_session_terminate (priv->session,
- TP_CHANNEL_GROUP_CHANGE_REASON_NONE, NULL, NULL);
+ JINGLE_REASON_UNKNOWN, NULL, NULL);
tp_svc_channel_emit_closed (self);
}
@@ -1284,6 +1288,30 @@ _find_stream_by_id (GabbleMediaChannel *chan,
return NULL;
}
+static GabbleMediaStream *
+_find_stream_by_content (GabbleMediaChannel *chan,
+ GabbleJingleContent *content)
+{
+ GabbleMediaChannelPrivate *priv;
+ guint i;
+
+ g_assert (GABBLE_IS_MEDIA_CHANNEL (chan));
+
+ priv = chan->priv;
+
+ for (i = 0; i < priv->streams->len; i++)
+ {
+ GabbleMediaStream *stream = g_ptr_array_index (priv->streams, i);
+ GabbleJingleContent *c = GABBLE_JINGLE_CONTENT (
+ gabble_media_stream_get_content (stream));
+
+ if (content == c)
+ return stream;
+ }
+
+ return NULL;
+}
+
/**
* gabble_media_channel_remove_streams
*
@@ -2184,7 +2212,7 @@ static gboolean
gabble_media_channel_remove_member (GObject *obj,
TpHandle handle,
const gchar *message,
- guint reason,
+ TpChannelGroupChangeReason reason,
GError **error)
{
GabbleMediaChannel *chan = GABBLE_MEDIA_CHANNEL (obj);
@@ -2206,15 +2234,34 @@ gabble_media_channel_remove_member (GObject *obj,
}
else
{
- /* Terminate can fail if the UI provides a reason that makes no sense,
- * like Invited.
- */
- if (!gabble_jingle_session_terminate (priv->session, reason, message,
- error))
+ JingleReason jingle_reason = JINGLE_REASON_UNKNOWN;
+
+ switch (reason)
{
+ case TP_CHANNEL_GROUP_CHANGE_REASON_NONE:
+ jingle_reason = JINGLE_REASON_UNKNOWN;
+ break;
+ case TP_CHANNEL_GROUP_CHANGE_REASON_OFFLINE:
+ jingle_reason = JINGLE_REASON_GONE;
+ break;
+ case TP_CHANNEL_GROUP_CHANGE_REASON_BUSY:
+ jingle_reason = JINGLE_REASON_BUSY;
+ break;
+ case TP_CHANNEL_GROUP_CHANGE_REASON_ERROR:
+ jingle_reason = JINGLE_REASON_GENERAL_ERROR;
+ break;
+ case TP_CHANNEL_GROUP_CHANGE_REASON_NO_ANSWER:
+ jingle_reason = JINGLE_REASON_TIMEOUT;
+ break;
+ default:
+ g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT,
+ "%u doesn't make sense as a reason to end a call", reason);
g_object_unref (chan);
return FALSE;
}
+
+ gabble_jingle_session_terminate (priv->session, jingle_reason, message,
+ error);
}
/* Remove CanAdd if it was there for the deprecated anonymous channel
@@ -2239,10 +2286,91 @@ copy_stream_list (GabbleMediaChannel *channel)
return gabble_g_ptr_array_copy (channel->priv->streams);
}
+
+/* return TRUE when the jingle reason is reason enough to raise a
+ * StreamError */
+static gboolean
+extract_media_stream_error_from_jingle_reason (JingleReason jingle_reason,
+ TpMediaStreamError *stream_error)
+{
+ TpMediaStreamError _stream_error;
+
+ /* TODO: Make a better mapping with more distinction of possible errors */
+ switch (jingle_reason)
+ {
+ case JINGLE_REASON_CONNECTIVITY_ERROR:
+ _stream_error = TP_MEDIA_STREAM_ERROR_NETWORK_ERROR;
+ break;
+ case JINGLE_REASON_MEDIA_ERROR:
+ _stream_error = TP_MEDIA_STREAM_ERROR_MEDIA_ERROR;
+ break;
+ case JINGLE_REASON_FAILED_APPLICATION:
+ _stream_error = TP_MEDIA_STREAM_ERROR_CODEC_NEGOTIATION_FAILED;
+ break;
+ case JINGLE_REASON_GENERAL_ERROR:
+ _stream_error = TP_MEDIA_STREAM_ERROR_UNKNOWN;
+ break;
+ default:
+ {
+ if (stream_error != NULL)
+ *stream_error = TP_MEDIA_STREAM_ERROR_UNKNOWN;
+
+ return FALSE;
+ }
+ }
+
+ if (stream_error != NULL)
+ *stream_error = _stream_error;
+
+ return TRUE;
+}
+
+static JingleReason
+media_stream_error_to_jingle_reason (TpMediaStreamError stream_error)
+{
+ switch (stream_error)
+ {
+ case TP_MEDIA_STREAM_ERROR_NETWORK_ERROR:
+ return JINGLE_REASON_CONNECTIVITY_ERROR;
+ case TP_MEDIA_STREAM_ERROR_MEDIA_ERROR:
+ return JINGLE_REASON_MEDIA_ERROR;
+ case TP_MEDIA_STREAM_ERROR_CODEC_NEGOTIATION_FAILED:
+ return JINGLE_REASON_FAILED_APPLICATION;
+ default:
+ return JINGLE_REASON_GENERAL_ERROR;
+ }
+}
+
+static TpChannelGroupChangeReason
+jingle_reason_to_group_change_reason (JingleReason jingle_reason)
+{
+ switch (jingle_reason)
+ {
+ case JINGLE_REASON_BUSY:
+ return TP_CHANNEL_GROUP_CHANGE_REASON_BUSY;
+ case JINGLE_REASON_GONE:
+ return TP_CHANNEL_GROUP_CHANGE_REASON_OFFLINE;
+ case JINGLE_REASON_TIMEOUT:
+ return TP_CHANNEL_GROUP_CHANGE_REASON_NO_ANSWER;
+ case JINGLE_REASON_CONNECTIVITY_ERROR:
+ case JINGLE_REASON_FAILED_APPLICATION:
+ case JINGLE_REASON_FAILED_TRANSPORT:
+ case JINGLE_REASON_GENERAL_ERROR:
+ case JINGLE_REASON_MEDIA_ERROR:
+ case JINGLE_REASON_SECURITY_ERROR:
+ case JINGLE_REASON_INCOMPATIBLE_PARAMETERS:
+ case JINGLE_REASON_UNSUPPORTED_APPLICATIONS:
+ case JINGLE_REASON_UNSUPPORTED_TRANSPORTS:
+ return TP_CHANNEL_GROUP_CHANGE_REASON_ERROR;
+ default:
+ return TP_CHANNEL_GROUP_CHANGE_REASON_NONE;
+ }
+}
+
static void
session_terminated_cb (GabbleJingleSession *session,
gboolean local_terminator,
- TpChannelGroupChangeReason reason,
+ JingleReason jingle_reason,
const gchar *text,
gpointer user_data)
{
@@ -2273,7 +2401,8 @@ session_terminated_cb (GabbleJingleSession *session,
tp_intset_add (set, peer);
tp_group_mixin_change_members ((GObject *) channel,
- text, NULL, set, NULL, NULL, terminator, reason);
+ text, NULL, set, NULL, NULL, terminator,
+ jingle_reason_to_group_change_reason (jingle_reason));
tp_intset_destroy (set);
@@ -2289,8 +2418,28 @@ session_terminated_cb (GabbleJingleSession *session,
{
GPtrArray *tmp = copy_stream_list (channel);
+ guint i;
+ TpMediaStreamError stream_error = TP_MEDIA_STREAM_ERROR_UNKNOWN;
+ gboolean is_error = extract_media_stream_error_from_jingle_reason (
+ jingle_reason, &stream_error);
- g_ptr_array_foreach (tmp, (GFunc) gabble_media_stream_close, NULL);
+ for (i = 0; i < tmp->len; i++)
+ {
+ GabbleMediaStream *stream = tmp->pdata[i];
+
+ if (is_error)
+ {
+ guint id;
+
+ DEBUG ("emitting stream error");
+
+ g_object_get (stream, "id", &id, NULL);
+ tp_svc_channel_type_streamed_media_emit_stream_error (channel, id,
+ stream_error, text);
+ }
+
+ gabble_media_stream_close (stream);
+ }
/* All the streams should have closed. */
g_assert (priv->streams->len == 0);
@@ -2430,8 +2579,13 @@ stream_error_cb (GabbleMediaStream *stream,
* so we can dispose of the stream)
*/
c = gabble_media_stream_get_content (stream);
- gabble_jingle_session_remove_content (priv->session,
- (GabbleJingleContent *) c);
+
+ if (errno == TP_MEDIA_STREAM_ERROR_CODEC_NEGOTIATION_FAILED)
+ gabble_jingle_content_reject ((GabbleJingleContent *) c,
+ JINGLE_REASON_FAILED_APPLICATION);
+ else
+ gabble_jingle_session_remove_content (priv->session,
+ (GabbleJingleContent *) c);
}
else
{
@@ -2442,7 +2596,7 @@ stream_error_cb (GabbleMediaStream *stream,
*/
DEBUG ("Terminating call in response to stream error");
gabble_jingle_session_terminate (priv->session,
- TP_CHANNEL_GROUP_CHANGE_REASON_ERROR, message, NULL);
+ media_stream_error_to_jingle_reason (errno), message, NULL);
}
g_list_free (contents);
@@ -2759,6 +2913,30 @@ create_stream_from_content (GabbleMediaChannel *self,
}
static void
+session_content_rejected_cb (GabbleJingleSession *session,
+ GabbleJingleContent *c, JingleReason reason, const gchar *message,
+ gpointer user_data)
+{
+ GabbleMediaChannel *chan = GABBLE_MEDIA_CHANNEL (user_data);
+ GabbleMediaStream *stream = _find_stream_by_content (chan, c);
+ TpMediaStreamError stream_error = TP_MEDIA_STREAM_ERROR_UNKNOWN;
+ guint id = 0;
+
+ DEBUG (" ");
+
+ g_return_if_fail (stream != NULL);
+
+ g_object_get (stream,
+ "id", &id,
+ NULL);
+
+ extract_media_stream_error_from_jingle_reason (reason, &stream_error);
+
+ tp_svc_channel_type_streamed_media_emit_stream_error (chan, id, stream_error,
+ message);
+}
+
+static void
session_new_content_cb (GabbleJingleSession *session,
GabbleJingleContent *c, gpointer user_data)
{
diff --git a/src/protocol.c b/src/protocol.c
index c8b4c83a5..03eefa7c3 100644
--- a/src/protocol.c
+++ b/src/protocol.c
@@ -346,6 +346,17 @@ get_connection_details (TpBaseProtocol *self,
}
}
+static GStrv
+dup_authentication_types (TpBaseProtocol *self)
+{
+ const gchar * const types[] = {
+ TP_IFACE_CHANNEL_TYPE_SERVER_TLS_CONNECTION,
+ TP_IFACE_CHANNEL_INTERFACE_SASL_AUTHENTICATION,
+ NULL };
+
+ return g_strdupv ((GStrv) types);
+}
+
static void
gabble_jabber_protocol_class_init (GabbleJabberProtocolClass *klass)
{
@@ -359,6 +370,7 @@ gabble_jabber_protocol_class_init (GabbleJabberProtocolClass *klass)
base_class->get_interfaces = get_interfaces;
base_class->get_connection_details = get_connection_details;
base_class->get_statuses = get_presence_statuses;
+ base_class->dup_authentication_types = dup_authentication_types;
}
TpBaseProtocol *
diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am
index e0dec8ebf..7278cd882 100644
--- a/tests/twisted/Makefile.am
+++ b/tests/twisted/Makefile.am
@@ -159,6 +159,8 @@ TWISTED_JINGLE_TESTS = \
jingle/payload-types.py \
jingle/preload-caps-crash.py \
jingle/session-id-collision.py \
+ jingle/stream-errors-on-terminate.py \
+ jingle/stream-errors-on-content-reject.py \
jingle/stream-handler-error.py \
jingle/test-content-adding-removal.py \
jingle/test-description-info.py \
diff --git a/tests/twisted/constants.py b/tests/twisted/constants.py
index 8d8db88ef..e8d476b7e 100644
--- a/tests/twisted/constants.py
+++ b/tests/twisted/constants.py
@@ -421,3 +421,12 @@ DELIVERY_REPORTING_SUPPORT_FLAGS_RECEIVE_FAILURES = 1
DELIVERY_REPORTING_SUPPORT_FLAGS_RECEIVE_SUCCESSES = 2
DELIVERY_REPORTING_SUPPORT_FLAGS_RECEIVE_READ = 4
DELIVERY_REPORTING_SUPPORT_FLAGS_RECEIVE_DELETED = 8
+
+MEDIA_STREAM_ERROR_UNKNOWN = 0
+MEDIA_STREAM_ERROR_EOS = 1
+MEDIA_STREAM_ERROR_CODEC_NEGOTIATION_FAILED = 2
+MEDIA_STREAM_ERROR_CONNECTION_FAILED = 3
+MEDIA_STREAM_ERROR_NETWORK_ERROR = 4
+MEDIA_STREAM_ERROR_NO_CODECS = 5
+MEDIA_STREAM_ERROR_INVALID_CM_BEHAVIOR = 6
+MEDIA_STREAM_ERROR_MEDIA_ERROR = 7
diff --git a/tests/twisted/jingle/incoming-call-stream-error.py b/tests/twisted/jingle/incoming-call-stream-error.py
index b41f6f32a..969b9ddda 100644
--- a/tests/twisted/jingle/incoming-call-stream-error.py
+++ b/tests/twisted/jingle/incoming-call-stream-error.py
@@ -10,9 +10,25 @@ from servicetest import EventPattern, assertEquals, make_channel_proxy
from jingletest2 import JingleTest2, test_all_dialects
import constants as cs
-MEDIA_STREAM_ERROR_CONNECTION_FAILED = 3
+from twisted.words.xish import xpath
-def test(jp, q, bus, conn, stream):
+def _session_terminate_predicate(event, reason, msg, jp):
+ matches = jp.match_jingle_action(event.query, 'session-terminate')
+
+ if matches and jp.is_modern_jingle():
+ reason = xpath.queryForNodes("/iq"
+ "/jingle[@action='session-terminate']"
+ "/reason/%s" % reason,
+ event.stanza)
+ reason_text = xpath.queryForString("/iq/jingle/reason/text",
+ event.stanza)
+
+ return bool(reason) and reason_text == msg
+
+ return matches
+
+def _test(jp, q, bus, conn, stream,
+ jingle_reason, group_change_reason, stream_error):
jt = JingleTest2(jp, conn, q, stream, 'test@localhost', 'foo@bar.com/Foo')
jt.prepare()
self_handle = conn.GetSelfHandle()
@@ -29,6 +45,8 @@ def test(jp, q, bus, conn, stream):
assertEquals(remote_handle, new_channel.args[3])
assertEquals('rtp', new_session_handler.args[1])
+ channel_path = new_channel.args[0]
+
# Client calls Ready on new session handler.
session_handler = make_channel_proxy(
conn, new_session_handler.args[0], 'Media.SessionHandler')
@@ -36,21 +54,48 @@ def test(jp, q, bus, conn, stream):
# Client gets notified about a newly created stream...
new_stream_handler = q.expect('dbus-signal', signal='NewStreamHandler')
+ stream_id = new_stream_handler.args[1]
stream_handler = make_channel_proxy(
conn, new_stream_handler.args[0], 'Media.StreamHandler')
+ stream_handler.NewNativeCandidate("fake", jt.get_remote_transports_dbus())
+ stream_handler.Ready(jt.dbusify_codecs([("FOO", 5, 8000, {})]))
+
+ q.expect('dbus-signal', signal='SetRemoteCodecs')
+
+ msg = u"o noes"
+
# ...but something goes wrong.
- stream_handler.Error(MEDIA_STREAM_ERROR_CONNECTION_FAILED, "o noes")
+ stream_handler.Error(stream_error, msg)
+ q.expect("stream-iq", iq_type="set",
+ predicate=lambda x: _session_terminate_predicate(x, jingle_reason,
+ msg, jp))
# Bye bye members.
- q.expect('dbus-signal', signal='MembersChanged',
- args=[u'o noes', [], [self_handle, remote_handle], [], [], self_handle,
- cs.GC_REASON_ERROR])
- # Bye bye stream.
+ mc = q.expect('dbus-signal', signal='MembersChanged',
+ interface=cs.CHANNEL_IFACE_GROUP, path=channel_path,
+ args=[msg, [], [self_handle, remote_handle], [],
+ [], self_handle, group_change_reason])
+
+ q.expect('dbus-signal', signal='StreamError',
+ interface=cs.CHANNEL_TYPE_STREAMED_MEDIA,
+ args=[stream_id, stream_error, msg])
+
+ # Bye bye stream
q.expect('dbus-signal', signal='Close')
q.expect('dbus-signal', signal='StreamRemoved')
+
# Bye bye channel.
q.expect('dbus-signal', signal='Closed')
q.expect('dbus-signal', signal='ChannelClosed')
+def test_connection_error(jp, q, bus, conn, stream):
+ _test(jp, q, bus, conn, stream, "connectivity-error", cs.GC_REASON_ERROR,
+ cs.MEDIA_STREAM_ERROR_NETWORK_ERROR)
+
+def test_codec_negotiation_fail(jp, q, bus, conn, stream):
+ _test(jp, q, bus, conn, stream, "failed-application", cs.GC_REASON_ERROR,
+ cs.MEDIA_STREAM_ERROR_CODEC_NEGOTIATION_FAILED)
+
if __name__ == '__main__':
- test_all_dialects(test)
+ test_all_dialects(test_connection_error)
+ test_all_dialects(test_codec_negotiation_fail)
diff --git a/tests/twisted/jingle/stream-errors-on-content-reject.py b/tests/twisted/jingle/stream-errors-on-content-reject.py
new file mode 100644
index 000000000..27ea726f9
--- /dev/null
+++ b/tests/twisted/jingle/stream-errors-on-content-reject.py
@@ -0,0 +1,240 @@
+"""
+Test StreamError events when new content is rejected in-call.
+"""
+
+import dbus
+
+from gabbletest import make_result_iq, sync_stream, exec_test
+from servicetest import (
+ make_channel_proxy, unwrap, EventPattern, assertEquals, assertLength)
+from jingletest2 import JingleTest2, JingleProtocol031
+import constants as cs
+
+from twisted.words.xish import xpath
+
+def _content_reject_predicate(event):
+ reason = xpath.queryForNodes("/iq"
+ "/jingle[@action='content-reject']"
+ "/reason/failed-application",
+ event.stanza)
+
+ return bool(reason)
+
+def _start_audio_session(jp, q, bus, conn, stream, incoming):
+ jt = JingleTest2(jp, conn, q, stream, 'test@localhost', 'foo@bar.com/Foo')
+ jt.prepare()
+
+ self_handle = conn.GetSelfHandle()
+ remote_handle = conn.RequestHandles(cs.HT_CONTACT, [jt.peer])[0]
+
+ if incoming:
+ jt.incoming_call()
+ else:
+ ret = conn.Requests.CreateChannel(
+ { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_STREAMED_MEDIA,
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.TARGET_HANDLE: remote_handle,
+ cs.INITIAL_AUDIO: True
+ })
+
+ nc, e = q.expect_many(
+ EventPattern('dbus-signal', signal='NewChannel',
+ predicate=lambda e: cs.CHANNEL_TYPE_CONTACT_LIST not in e.args),
+ EventPattern('dbus-signal', signal='NewSessionHandler'))
+
+ path = nc.args[0]
+
+ media_chan = make_channel_proxy(conn, path, 'Channel.Interface.Group')
+ media_iface = make_channel_proxy(conn, path, 'Channel.Type.StreamedMedia')
+
+ # S-E was notified about new session handler, and calls Ready on it
+ session_handler = make_channel_proxy(conn, e.args[0],
+ 'Media.SessionHandler')
+ session_handler.Ready()
+
+ nsh_event = q.expect('dbus-signal', signal='NewStreamHandler')
+
+ # S-E gets notified about a newly-created stream
+ stream_handler = make_channel_proxy(conn, nsh_event.args[0],
+ 'Media.StreamHandler')
+
+ group_props = media_chan.GetAll(
+ cs.CHANNEL_IFACE_GROUP, dbus_interface=dbus.PROPERTIES_IFACE)
+
+ if incoming:
+ assertEquals([remote_handle], group_props['Members'])
+ assertEquals(unwrap(group_props['LocalPendingMembers']),
+ [(self_handle, remote_handle, cs.GC_REASON_INVITED, '')])
+ else:
+ assertEquals([self_handle], group_props['Members'])
+
+ streams = media_chan.ListStreams(
+ dbus_interface=cs.CHANNEL_TYPE_STREAMED_MEDIA)
+
+ stream_id = streams[0][0]
+
+ stream_handler.NewNativeCandidate("fake", jt.get_remote_transports_dbus())
+ stream_handler.Ready(jt.dbusify_codecs([("FOO", 5, 8000, {})]))
+
+ msg = u"None of the codecs are good for us, damn!"
+
+ expected_events = []
+
+ if incoming:
+ stream_handler.StreamState(cs.MEDIA_STREAM_STATE_CONNECTED)
+ stream_handler.SupportedCodecs(jt.get_audio_codecs_dbus())
+
+ e = q.expect('stream-iq', predicate=jp.action_predicate('transport-info'))
+ assertEquals(jt.peer, e.query['initiator'])
+ content = xpath.queryForNodes('/iq/jingle/content', e.stanza)[0]
+ assertEquals('initiator', content['creator'])
+
+ stream.send(make_result_iq(stream, e.stanza))
+
+ media_chan.AddMembers([self_handle], 'accepted')
+
+ memb, acc, _, _, _ = q.expect_many(
+ EventPattern('dbus-signal', signal='MembersChanged',
+ args=[u'', [self_handle], [], [], [], self_handle,
+ cs.GC_REASON_NONE]),
+ EventPattern('stream-iq',
+ predicate=jp.action_predicate('session-accept')),
+ EventPattern('dbus-signal', signal='SetStreamSending',
+ args=[True]),
+ EventPattern('dbus-signal', signal='SetStreamPlaying',
+ args=[True]),
+ EventPattern('dbus-signal', signal='StreamDirectionChanged',
+ args=[stream_id,
+ cs.MEDIA_STREAM_DIRECTION_BIDIRECTIONAL, 0]))
+
+ stream.send(make_result_iq(stream, acc.stanza))
+
+ active_event = jp.rtp_info_event("active")
+ if active_event is not None:
+ q.expect_many(active_event)
+
+ members = media_chan.GetMembers()
+ assert set(members) == set([self_handle, remote_handle]), members
+ else:
+ stream_handler.StreamState(cs.MEDIA_STREAM_STATE_CONNECTED)
+ session_initiate = q.expect(
+ 'stream-iq',
+ predicate=jp.action_predicate('session-initiate'))
+
+ q.expect('dbus-signal', signal='MembersChanged', path=path,
+ args=['', [], [], [], [remote_handle], self_handle,
+ cs.GC_REASON_INVITED])
+
+ jt.parse_session_initiate(session_initiate.query)
+ stream.send(jp.xml(jp.ResultIq('test@localhost',
+ session_initiate.stanza, [])))
+
+ jt.accept()
+
+ q.expect_many(
+ EventPattern('stream-iq', iq_type='result'),
+ # Call accepted
+ EventPattern('dbus-signal', signal='MembersChanged',
+ args=['', [remote_handle], [], [], [], remote_handle,
+ cs.GC_REASON_NONE]),
+ )
+ return jt, media_iface
+
+def _start_audio_session_outgoing(jp, q, bus, conn, stream):
+ return _start_audio_session(jp, q, bus, conn, stream, False)
+
+def _start_audio_session_incoming(jp, q, bus, conn, stream):
+ return _start_audio_session(jp, q, bus, conn, stream, True)
+
+def _remote_content_add(jp, q, bus, conn, stream, initiate_call_func):
+ jt, chan = initiate_call_func(jp, q, bus, conn, stream)
+
+ video_codecs = [
+ jp.PayloadType(name, str(rate), str(id), parameters) \
+ for (name, id, rate, parameters) in jt.video_codecs]
+
+ node = jp.SetIq(jt.peer, jt.jid, [
+ jp.Jingle(jt.sid, jt.peer, 'content-add', [
+ jp.Content(
+ 'videostream', 'initiator', 'both',
+ jp.Description('video', video_codecs),
+ jp.TransportGoogleP2P()) ]) ])
+ stream.send(jp.xml(node))
+
+ _, nsh = q.expect_many(
+ EventPattern('dbus-signal', signal='StreamAdded'),
+ EventPattern('dbus-signal', signal='NewStreamHandler'))
+
+ stream_handler_path, stream_id, media_type, direction = nsh.args
+
+ video_handler = make_channel_proxy(conn, stream_handler_path,
+ 'Media.StreamHandler')
+
+ video_handler.NewNativeCandidate("fake",
+ jt.get_remote_transports_dbus())
+ video_handler.Ready(jt.dbusify_codecs([("FOO", 5, 8000, {})]))
+
+ msg = u"None of the codecs are good for us, damn!"
+
+ video_handler.Error(cs.MEDIA_STREAM_ERROR_CODEC_NEGOTIATION_FAILED, msg)
+
+ q.expect_many(
+ EventPattern('dbus-signal', signal='StreamError',
+ args=[stream_id,
+ cs.MEDIA_STREAM_ERROR_CODEC_NEGOTIATION_FAILED,
+ msg]),
+ EventPattern('stream-iq', predicate=_content_reject_predicate))
+
+def _local_content_add(jp, q, bus, conn, stream, initiate_call_func):
+ jt, chan = initiate_call_func(jp, q, bus, conn, stream)
+
+ remote_handle = conn.RequestHandles(cs.HT_CONTACT, [jt.peer])[0]
+
+ chan.RequestStreams(remote_handle, [cs.MEDIA_STREAM_TYPE_VIDEO])
+
+ nsh = q.expect('dbus-signal', signal='NewStreamHandler')
+ stream_handler_path, stream_id, media_type, direction = nsh.args
+ video_handler = make_channel_proxy(conn, stream_handler_path,
+ 'Media.StreamHandler')
+
+ video_handler.NewNativeCandidate("fake", jt.get_remote_transports_dbus())
+ video_handler.Ready(jt.get_audio_codecs_dbus())
+ video_handler.StreamState(cs.MEDIA_STREAM_STATE_CONNECTED)
+
+ e = q.expect('stream-iq', predicate=jp.action_predicate('content-add'))
+ c = e.query.firstChildElement()
+ stream.send(make_result_iq(stream, e.stanza))
+
+ node = jp.SetIq(jt.peer, jt.jid, [
+ jp.Jingle(jt.sid, jt.peer, 'content-reject', [
+ ('reason', None, {}, [
+ ('failed-application', None, {}, [])]),
+ jp.Content(c['name'], c['creator'], c['senders']) ]) ])
+ stream.send(jp.xml(node))
+
+ q.expect('dbus-signal', signal='StreamError',
+ args=[stream_id,
+ cs.MEDIA_STREAM_ERROR_CODEC_NEGOTIATION_FAILED,
+ ""]),
+
+def test_remote_content_add_incoming(jp, q, bus, conn, stream):
+ _remote_content_add(jp, q, bus, conn, stream,
+ _start_audio_session_incoming)
+
+def test_remote_content_add_outgoing(jp, q, bus, conn, stream):
+ _remote_content_add(jp, q, bus, conn, stream,
+ _start_audio_session_outgoing)
+
+def test_local_content_add_incoming(jp, q, bus, conn, stream):
+ _local_content_add(jp, q, bus, conn, stream, _start_audio_session_incoming)
+
+def test_local_content_add_outgoing(jp, q, bus, conn, stream):
+ _local_content_add(jp, q, bus, conn, stream, _start_audio_session_outgoing)
+
+if __name__ == '__main__':
+ for f in (test_local_content_add_incoming,
+ test_local_content_add_outgoing,
+ test_remote_content_add_incoming,
+ test_remote_content_add_outgoing):
+ exec_test(
+ lambda q, b, c, s: f(JingleProtocol031(), q, b, c, s))
diff --git a/tests/twisted/jingle/stream-errors-on-terminate.py b/tests/twisted/jingle/stream-errors-on-terminate.py
new file mode 100644
index 000000000..8e46473bc
--- /dev/null
+++ b/tests/twisted/jingle/stream-errors-on-terminate.py
@@ -0,0 +1,134 @@
+"""
+Test StreamError events and on session terminate, both directions.
+"""
+
+import dbus
+
+from gabbletest import make_result_iq, sync_stream, exec_test
+from servicetest import (
+ make_channel_proxy, unwrap, EventPattern, assertEquals, assertLength)
+from jingletest2 import JingleTest2, JingleProtocol031
+import constants as cs
+
+from twisted.words.xish import xpath
+
+def _session_terminate_predicate(event, msg):
+ reason = xpath.queryForNodes("/iq"
+ "/jingle[@action='session-terminate']"
+ "/reason/failed-application",
+ event.stanza)
+ reason_text = xpath.queryForString("/iq/jingle/reason/text",
+ event.stanza)
+
+ return reason is not None and reason_text == msg
+
+def _test_terminate_reason(jp, q, bus, conn, stream, incoming):
+ jt = JingleTest2(jp, conn, q, stream, 'test@localhost', 'foo@bar.com/Foo')
+ jt.prepare()
+
+ self_handle = conn.GetSelfHandle()
+ remote_handle = conn.RequestHandles(cs.HT_CONTACT, [jt.peer])[0]
+
+ if incoming:
+ jt.incoming_call()
+ else:
+ ret = conn.Requests.CreateChannel(
+ { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_STREAMED_MEDIA,
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.TARGET_HANDLE: remote_handle,
+ cs.INITIAL_AUDIO: True
+ })
+
+ nc, e = q.expect_many(
+ EventPattern('dbus-signal', signal='NewChannel',
+ predicate=lambda e: cs.CHANNEL_TYPE_CONTACT_LIST not in e.args),
+ EventPattern('dbus-signal', signal='NewSessionHandler'))
+
+ path = nc.args[0]
+
+ media_chan = make_channel_proxy(conn, path, 'Channel.Interface.Group')
+ media_iface = make_channel_proxy(conn, path, 'Channel.Type.StreamedMedia')
+
+ # S-E was notified about new session handler, and calls Ready on it
+ session_handler = make_channel_proxy(conn, e.args[0],
+ 'Media.SessionHandler')
+ session_handler.Ready()
+
+ nsh_event = q.expect('dbus-signal', signal='NewStreamHandler')
+
+ # S-E gets notified about a newly-created stream
+ stream_handler = make_channel_proxy(conn, nsh_event.args[0],
+ 'Media.StreamHandler')
+
+ group_props = media_chan.GetAll(
+ cs.CHANNEL_IFACE_GROUP, dbus_interface=dbus.PROPERTIES_IFACE)
+
+ if incoming:
+ assertEquals([remote_handle], group_props['Members'])
+ assertEquals(unwrap(group_props['LocalPendingMembers']),
+ [(self_handle, remote_handle, cs.GC_REASON_INVITED, '')])
+ else:
+ assertEquals([self_handle], group_props['Members'])
+
+ streams = media_chan.ListStreams(
+ dbus_interface=cs.CHANNEL_TYPE_STREAMED_MEDIA)
+
+ stream_id = streams[0][0]
+
+ stream_handler.NewNativeCandidate("fake", jt.get_remote_transports_dbus())
+ stream_handler.Ready(jt.dbusify_codecs([("FOO", 5, 8000, {})]))
+
+ msg = u"None of the codecs are good for us, damn!"
+
+ expected_events = []
+
+ if incoming:
+ q.expect('dbus-signal', signal='SetRemoteCodecs')
+ stream_handler.Error(cs.MEDIA_STREAM_ERROR_CODEC_NEGOTIATION_FAILED,
+ msg)
+ expected_events = [EventPattern(
+ "stream-iq", iq_type="set",
+ predicate=lambda x: _session_terminate_predicate(x, msg))]
+ rejector = self_handle
+ else:
+ stream_handler.StreamState(cs.MEDIA_STREAM_STATE_CONNECTED)
+ session_initiate = q.expect(
+ 'stream-iq',
+ predicate=jp.action_predicate('session-initiate'))
+
+ q.expect('dbus-signal', signal='MembersChanged', path=path,
+ args=['', [], [], [], [remote_handle], self_handle,
+ cs.GC_REASON_INVITED])
+
+ jt.parse_session_initiate(session_initiate.query)
+ stream.send(jp.xml(jp.ResultIq('test@localhost',
+ session_initiate.stanza, [])))
+ jt.terminate('failed-application', msg)
+ rejector = remote_handle
+
+ expected_events += [
+ EventPattern('dbus-signal', signal='StreamError',
+ interface=cs.CHANNEL_TYPE_STREAMED_MEDIA, path=path,
+ args=[stream_id,
+ cs.MEDIA_STREAM_ERROR_CODEC_NEGOTIATION_FAILED,
+ msg]),
+ EventPattern('dbus-signal', signal='MembersChanged',
+ interface=cs.CHANNEL_IFACE_GROUP, path=path,
+ args=[msg, [], [self_handle, remote_handle], [], [],
+ rejector, cs.GC_REASON_ERROR])]
+
+
+ q.expect_many(*expected_events)
+
+ q.expect('dbus-signal', signal='Closed', path=path)
+
+def test_terminate_outgoing(jp, q, bus, conn, stream):
+ _test_terminate_reason(jp, q, bus, conn, stream, False)
+
+def test_terminate_incoming(jp, q, bus, conn, stream):
+ _test_terminate_reason(jp, q, bus, conn, stream, True)
+
+if __name__ == '__main__':
+ for f in (test_terminate_incoming, test_terminate_outgoing):
+ exec_test(
+ lambda q, b, c, s: f(JingleProtocol031(), q, b, c, s))
diff --git a/tests/twisted/jingle/test-outgoing-call-rejected.py b/tests/twisted/jingle/test-outgoing-call-rejected.py
index d629b2af9..3f0ef4ad4 100644
--- a/tests/twisted/jingle/test-outgoing-call-rejected.py
+++ b/tests/twisted/jingle/test-outgoing-call-rejected.py
@@ -8,7 +8,8 @@ from servicetest import make_channel_proxy, assertEquals
import constants as cs
from jingletest2 import JingleTest2, test_all_dialects
-def test(jp, q, bus, conn, stream):
+def _test(jp, q, bus, conn, stream,
+ jingle_reason, group_change_reason, stream_error):
remote_jid = 'foo@bar.com/Foo'
jt = JingleTest2(jp, conn, q, stream, 'test@localhost', remote_jid)
@@ -47,7 +48,7 @@ def test(jp, q, bus, conn, stream):
text = u"begone!"
jt.parse_session_initiate(e.query)
- jt.terminate(reason="busy", text=text)
+ jt.terminate(reason=jingle_reason, text=text)
mc = q.expect('dbus-signal', signal='MembersChanged')
message, added, removed, lp, rp, actor, reason = mc.args
@@ -59,9 +60,21 @@ def test(jp, q, bus, conn, stream):
assert actor == remote_handle, (actor, remote_handle)
if jp.is_modern_jingle():
assertEquals(text, message)
- assertEquals(cs.GC_REASON_BUSY, reason)
+ assertEquals(group_change_reason, reason)
+
+ if jp.is_modern_jingle() and stream_error:
+ se = q.expect('dbus-signal', signal='StreamError')
+ assertEquals(stream_error, se.args[1])
q.expect('dbus-signal', signal='Close') #XXX - match against the path
+def test_busy(jp, q, bus, conn, stream):
+ _test(jp, q, bus, conn, stream, "busy", cs.GC_REASON_BUSY, None)
+
+def test_codec_fail(jp, q, bus, conn, stream):
+ _test(jp, q, bus, conn, stream, "failed-application", cs.GC_REASON_ERROR,
+ cs.MEDIA_STREAM_ERROR_CODEC_NEGOTIATION_FAILED)
+
if __name__ == '__main__':
- test_all_dialects(test)
+ test_all_dialects(test_busy)
+ test_all_dialects(test_codec_fail)
diff --git a/tools/telepathy.am b/tools/telepathy.am
index 1ed13078f..971502639 100644
--- a/tools/telepathy.am
+++ b/tools/telepathy.am
@@ -7,8 +7,9 @@ dist-hook:
fi
distcheck-hook:
- @case @VERSION@ in \
- *.*.*.*) ;; \
+ @test "z$(CHECK_FOR_UNRELEASED)" = z || \
+ case @VERSION@ in \
+ *.*.*.*|*+) ;; \
*) \
if grep -r UNRELEASED $(CHECK_FOR_UNRELEASED); \
then \
@@ -20,7 +21,7 @@ distcheck-hook:
_is-release-check:
@case @VERSION@ in \
- (*.*.*.*) \
+ (*.*.*.*|*+) \
echo "Hey! @VERSION@ is not a release!" >&2; \
exit 2; \
;; \
@@ -29,6 +30,10 @@ _is-release-check:
echo "Hey! Your tree is dirty! No release for you." >&2; \
exit 2; \
fi
+ @if ! git diff --cached --no-ext-diff --quiet --exit-code; then \
+ echo "Hey! You have changes staged! No release for you." >&2; \
+ exit 2; \
+ fi
%.tar.gz.asc: %.tar.gz
$(AM_V_GEN)gpg --detach-sign --armor $@
@@ -59,6 +64,6 @@ maintainer-make-release: maintainer-prepare-release maintainer-upload-release
@echo "Now:"
@echo " • bump the nano-version;"
@echo " • push the branch and tags upstream; and"
- @echo " • send release-mail to <telepathy@freedesktop.org>."
+ @echo " • send release-mail to <telepathy@lists.freedesktop.org>."
## vim:set ft=automake: