mirror of
				https://github.com/asterisk/asterisk.git
				synced 2025-10-31 02:37:10 +00:00 
			
		
		
		
	Introduce a ChannelTransfer event and the ability to notify progress to ARI. Implement emitting this event from the PJSIP channel instead of handling the transfer in Asterisk when configured. Introduce a dialplan function to the PJSIP channel to switch between the "core" and "ari-only" behavior. UserNote: Call transfers on the PJSIP channel can now be controlled by ARI. This can be enabled by using the PJSIP_TRANSFER_HANDLING(ari-only) dialplan function.
		
			
				
	
	
		
			596 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			596 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /*
 | |
|  * Asterisk -- An open source telephony toolkit.
 | |
|  *
 | |
|  * Copyright (C) 2023, Commend International
 | |
|  *
 | |
|  * Maximilian Fridrich <m.fridrich@commend.com>
 | |
|  *
 | |
|  * See http://www.asterisk.org for more information about
 | |
|  * the Asterisk project. Please do not directly contact
 | |
|  * any of the maintainers of this project for assistance;
 | |
|  * the project provides a web site, mailing lists and IRC
 | |
|  * channels for your use.
 | |
|  *
 | |
|  * This program is free software, distributed under the terms of
 | |
|  * the GNU General Public License Version 2. See the LICENSE file
 | |
|  * at the top of the source tree.
 | |
|  */
 | |
| 
 | |
| /*! \file
 | |
|  *
 | |
|  * \brief Out-of-call refer support
 | |
|  *
 | |
|  * \author Maximilian Fridrich <m.fridrich@commend.com>
 | |
|  */
 | |
| 
 | |
| /*** MODULEINFO
 | |
| 	<support_level>core</support_level>
 | |
|  ***/
 | |
| 
 | |
| #include "asterisk.h"
 | |
| 
 | |
| #include "asterisk/_private.h"
 | |
| 
 | |
| #include "asterisk/module.h"
 | |
| #include "asterisk/datastore.h"
 | |
| #include "asterisk/pbx.h"
 | |
| #include "asterisk/manager.h"
 | |
| #include "asterisk/stasis_bridges.h"
 | |
| #include "asterisk/stasis_channels.h"
 | |
| #include "asterisk/strings.h"
 | |
| #include "asterisk/astobj2.h"
 | |
| #include "asterisk/vector.h"
 | |
| #include "asterisk/app.h"
 | |
| #include "asterisk/taskprocessor.h"
 | |
| #include "asterisk/refer.h"
 | |
| 
 | |
| struct refer_data {
 | |
| 	/* Stored in stuff[] at struct end */
 | |
| 	char *name;
 | |
| 	/* Stored separately */
 | |
| 	char *value;
 | |
| 	/* Holds name */
 | |
| 	char stuff[0];
 | |
| };
 | |
| 
 | |
| /*!
 | |
|  * \brief A refer.
 | |
|  */
 | |
| struct ast_refer {
 | |
| 	AST_DECLARE_STRING_FIELDS(
 | |
| 		/*! Where the refer is going */
 | |
| 		AST_STRING_FIELD(to);
 | |
| 		/*! Where we "say" the refer came from */
 | |
| 		AST_STRING_FIELD(from);
 | |
| 		/*! Where to refer to */
 | |
| 		AST_STRING_FIELD(refer_to);
 | |
| 		/*! An endpoint associated with this refer */
 | |
| 		AST_STRING_FIELD(endpoint);
 | |
| 		/*! The technology of the endpoint associated with this refer */
 | |
| 		AST_STRING_FIELD(tech);
 | |
| 	);
 | |
| 	/* Whether to refer to Asterisk itself, if refer_to is an Asterisk endpoint. */
 | |
| 	int to_self;
 | |
| 	/*! Technology/dialplan specific variables associated with the refer */
 | |
| 	struct ao2_container *vars;
 | |
| };
 | |
| 
 | |
| /*! \brief Lock for \c refer_techs vector */
 | |
| static ast_rwlock_t refer_techs_lock;
 | |
| 
 | |
| /*! \brief Vector of refer technologies */
 | |
| AST_VECTOR(, const struct ast_refer_tech *) refer_techs;
 | |
| 
 | |
| static int refer_data_cmp_fn(void *obj, void *arg, int flags)
 | |
| {
 | |
| 	const struct refer_data *object_left = obj;
 | |
| 	const struct refer_data *object_right = arg;
 | |
| 	const char *right_key = arg;
 | |
| 	int cmp;
 | |
| 
 | |
| 	switch (flags & OBJ_SEARCH_MASK) {
 | |
| 	case OBJ_SEARCH_OBJECT:
 | |
| 		right_key = object_right->name;
 | |
| 	case OBJ_SEARCH_KEY:
 | |
| 		cmp = strcasecmp(object_left->name, right_key);
 | |
| 		break;
 | |
| 	case OBJ_SEARCH_PARTIAL_KEY:
 | |
| 		cmp = strncasecmp(object_left->name, right_key, strlen(right_key));
 | |
| 		break;
 | |
| 	default:
 | |
| 		cmp = 0;
 | |
| 		break;
 | |
| 	}
 | |
| 	if (cmp) {
 | |
| 		return 0;
 | |
| 	}
 | |
| 	return CMP_MATCH;
 | |
| }
 | |
| 
 | |
| static void refer_data_destructor(void *obj)
 | |
| {
 | |
| 	struct refer_data *data = obj;
 | |
| 	ast_free(data->value);
 | |
| }
 | |
| 
 | |
| static void refer_destructor(void *obj)
 | |
| {
 | |
| 	struct ast_refer *refer = obj;
 | |
| 
 | |
| 	ast_string_field_free_memory(refer);
 | |
| 	ao2_cleanup(refer->vars);
 | |
| }
 | |
| 
 | |
| struct ast_refer *ast_refer_alloc(void)
 | |
| {
 | |
| 	struct ast_refer *refer;
 | |
| 
 | |
| 	if (!(refer = ao2_alloc_options(sizeof(*refer), refer_destructor, AO2_ALLOC_OPT_LOCK_NOLOCK))) {
 | |
| 		return NULL;
 | |
| 	}
 | |
| 
 | |
| 	if (ast_string_field_init(refer, 128)) {
 | |
| 		ao2_ref(refer, -1);
 | |
| 		return NULL;
 | |
| 	}
 | |
| 
 | |
| 	refer->vars = ao2_container_alloc_list(AO2_ALLOC_OPT_LOCK_MUTEX, 0,
 | |
| 		NULL, refer_data_cmp_fn);
 | |
| 	if (!refer->vars) {
 | |
| 		ao2_ref(refer, -1);
 | |
| 		return NULL;
 | |
| 	}
 | |
| 	refer->to_self = 0;
 | |
| 
 | |
| 	return refer;
 | |
| }
 | |
| 
 | |
| struct ast_refer *ast_refer_ref(struct ast_refer *refer)
 | |
| {
 | |
| 	ao2_ref(refer, 1);
 | |
| 	return refer;
 | |
| }
 | |
| 
 | |
| struct ast_refer *ast_refer_destroy(struct ast_refer *refer)
 | |
| {
 | |
| 	ao2_ref(refer, -1);
 | |
| 	return NULL;
 | |
| }
 | |
| 
 | |
| int ast_refer_set_to(struct ast_refer *refer, const char *fmt, ...)
 | |
| {
 | |
| 	va_list ap;
 | |
| 
 | |
| 	va_start(ap, fmt);
 | |
| 	ast_string_field_build_va(refer, to, fmt, ap);
 | |
| 	va_end(ap);
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| int ast_refer_set_from(struct ast_refer *refer, const char *fmt, ...)
 | |
| {
 | |
| 	va_list ap;
 | |
| 
 | |
| 	va_start(ap, fmt);
 | |
| 	ast_string_field_build_va(refer, from, fmt, ap);
 | |
| 	va_end(ap);
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| int ast_refer_set_refer_to(struct ast_refer *refer, const char *fmt, ...)
 | |
| {
 | |
| 	va_list ap;
 | |
| 
 | |
| 	va_start(ap, fmt);
 | |
| 	ast_string_field_build_va(refer, refer_to, fmt, ap);
 | |
| 	va_end(ap);
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| int ast_refer_set_to_self(struct ast_refer *refer, int val)
 | |
| {
 | |
| 	refer->to_self = val;
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| int ast_refer_set_tech(struct ast_refer *refer, const char *fmt, ...)
 | |
| {
 | |
| 	va_list ap;
 | |
| 
 | |
| 	va_start(ap, fmt);
 | |
| 	ast_string_field_build_va(refer, tech, fmt, ap);
 | |
| 	va_end(ap);
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| int ast_refer_set_endpoint(struct ast_refer *refer, const char *fmt, ...)
 | |
| {
 | |
| 	va_list ap;
 | |
| 
 | |
| 	va_start(ap, fmt);
 | |
| 	ast_string_field_build_va(refer, endpoint, fmt, ap);
 | |
| 	va_end(ap);
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| const char *ast_refer_get_refer_to(const struct ast_refer *refer)
 | |
| {
 | |
| 	return refer->refer_to;
 | |
| }
 | |
| 
 | |
| const char *ast_refer_get_from(const struct ast_refer *refer)
 | |
| {
 | |
| 	return refer->from;
 | |
| }
 | |
| 
 | |
| const char *ast_refer_get_to(const struct ast_refer *refer)
 | |
| {
 | |
| 	return refer->to;
 | |
| }
 | |
| 
 | |
| int ast_refer_get_to_self(const struct ast_refer *refer)
 | |
| {
 | |
| 	return refer->to_self;
 | |
| }
 | |
| 
 | |
| const char *ast_refer_get_tech(const struct ast_refer *refer)
 | |
| {
 | |
| 	return refer->tech;
 | |
| }
 | |
| 
 | |
| const char *ast_refer_get_endpoint(const struct ast_refer *refer)
 | |
| {
 | |
| 	return refer->endpoint;
 | |
| }
 | |
| 
 | |
| static struct refer_data *refer_data_new(const char *name)
 | |
| {
 | |
| 	struct refer_data *data;
 | |
| 	int name_len = strlen(name) + 1;
 | |
| 
 | |
| 	if ((data = ao2_alloc_options(name_len + sizeof(*data), refer_data_destructor, AO2_ALLOC_OPT_LOCK_NOLOCK))) {
 | |
| 		data->name = data->stuff;
 | |
| 		strcpy(data->name, name);
 | |
| 	}
 | |
| 
 | |
| 	return data;
 | |
| }
 | |
| 
 | |
| static struct refer_data *refer_data_find(struct ao2_container *vars, const char *name)
 | |
| {
 | |
| 	return ao2_find(vars, name, OBJ_SEARCH_KEY);
 | |
| }
 | |
| 
 | |
| char *ast_refer_get_var_and_unlink(struct ast_refer *refer, const char *name)
 | |
| {
 | |
| 	struct refer_data *data;
 | |
| 	char *val = NULL;
 | |
| 
 | |
| 	if (!(data = ao2_find(refer->vars, name, OBJ_SEARCH_KEY | OBJ_UNLINK))) {
 | |
| 		return NULL;
 | |
| 	}
 | |
| 
 | |
| 	val = ast_strdup(data->value);
 | |
| 	ao2_ref(data, -1);
 | |
| 
 | |
| 	return val;
 | |
| }
 | |
| 
 | |
| static int refer_set_var_full(struct ast_refer *refer, const char *name, const char *value)
 | |
| {
 | |
| 	struct refer_data *data;
 | |
| 
 | |
| 	if (!(data = refer_data_find(refer->vars, name))) {
 | |
| 		if (ast_strlen_zero(value)) {
 | |
| 			return 0;
 | |
| 		}
 | |
| 		if (!(data = refer_data_new(name))) {
 | |
| 			return -1;
 | |
| 		};
 | |
| 		data->value = ast_strdup(value);
 | |
| 
 | |
| 		ao2_link(refer->vars, data);
 | |
| 	} else {
 | |
| 		if (ast_strlen_zero(value)) {
 | |
| 			ao2_unlink(refer->vars, data);
 | |
| 		} else {
 | |
| 			ast_free(data->value);
 | |
| 			data->value = ast_strdup(value);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	ao2_ref(data, -1);
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| int ast_refer_set_var_outbound(struct ast_refer *refer, const char *name, const char *value)
 | |
| {
 | |
| 	return refer_set_var_full(refer, name, value);
 | |
| }
 | |
| 
 | |
| const char *ast_refer_get_var(struct ast_refer *refer, const char *name)
 | |
| {
 | |
| 	struct refer_data *data;
 | |
| 	const char *val = NULL;
 | |
| 
 | |
| 	if (!(data = refer_data_find(refer->vars, name))) {
 | |
| 		return NULL;
 | |
| 	}
 | |
| 
 | |
| 	val = data->value;
 | |
| 	ao2_ref(data, -1);
 | |
| 
 | |
| 	return val;
 | |
| }
 | |
| 
 | |
| struct ast_refer_var_iterator {
 | |
| 	struct ao2_iterator iter;
 | |
| 	struct refer_data *current_used;
 | |
| };
 | |
| 
 | |
| struct ast_refer_var_iterator *ast_refer_var_iterator_init(const struct ast_refer *refer)
 | |
| {
 | |
| 	struct ast_refer_var_iterator *iter;
 | |
| 
 | |
| 	iter = ast_calloc(1, sizeof(*iter));
 | |
| 	if (!iter) {
 | |
| 		return NULL;
 | |
| 	}
 | |
| 
 | |
| 	iter->iter = ao2_iterator_init(refer->vars, 0);
 | |
| 
 | |
| 	return iter;
 | |
| }
 | |
| 
 | |
| int ast_refer_var_iterator_next(struct ast_refer_var_iterator *iter, const char **name, const char **value)
 | |
| {
 | |
| 	struct refer_data *data;
 | |
| 
 | |
| 	if (!iter) {
 | |
| 		return 0;
 | |
| 	}
 | |
| 
 | |
| 	data = ao2_iterator_next(&iter->iter);
 | |
| 	if (!data) {
 | |
| 		return 0;
 | |
| 	}
 | |
| 
 | |
| 	*name = data->name;
 | |
| 	*value = data->value;
 | |
| 
 | |
| 	iter->current_used = data;
 | |
| 
 | |
| 	return 1;
 | |
| }
 | |
| 
 | |
| void ast_refer_var_unref_current(struct ast_refer_var_iterator *iter)
 | |
| {
 | |
| 	ao2_cleanup(iter->current_used);
 | |
| 	iter->current_used = NULL;
 | |
| }
 | |
| 
 | |
| void ast_refer_var_iterator_destroy(struct ast_refer_var_iterator *iter)
 | |
| {
 | |
| 	if (iter) {
 | |
| 		ao2_iterator_destroy(&iter->iter);
 | |
| 		ast_refer_var_unref_current(iter);
 | |
| 		ast_free(iter);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| /*!
 | |
|  * \internal \brief Find a \c ast_refer_tech by its technology name
 | |
|  *
 | |
|  * \param tech_name The name of the refer technology
 | |
|  *
 | |
|  * \note \c refer_techs should be locked via \c refer_techs_lock prior to
 | |
|  *       calling this function
 | |
|  *
 | |
|  * \retval NULL if no \ref ast_refer_tech has been registered
 | |
|  * \return \ref ast_refer_tech if registered
 | |
|  */
 | |
| static const struct ast_refer_tech *refer_find_by_tech_name(const char *tech_name)
 | |
| {
 | |
| 	const struct ast_refer_tech *current;
 | |
| 	int i;
 | |
| 
 | |
| 	for (i = 0; i < AST_VECTOR_SIZE(&refer_techs); i++) {
 | |
| 		current = AST_VECTOR_GET(&refer_techs, i);
 | |
| 		if (!strcmp(current->name, tech_name)) {
 | |
| 			return current;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return NULL;
 | |
| }
 | |
| 
 | |
| int ast_refer_send(struct ast_refer *refer)
 | |
| {
 | |
| 	char *tech_name = NULL;
 | |
| 	const struct ast_refer_tech *refer_tech;
 | |
| 	int res = -1;
 | |
| 
 | |
| 	if (ast_strlen_zero(refer->to)) {
 | |
| 		ao2_ref(refer, -1);
 | |
| 		return -1;
 | |
| 	}
 | |
| 
 | |
| 	tech_name = ast_strdupa(refer->to);
 | |
| 	tech_name = strsep(&tech_name, ":");
 | |
| 
 | |
| 	ast_rwlock_rdlock(&refer_techs_lock);
 | |
| 	refer_tech = refer_find_by_tech_name(tech_name);
 | |
| 
 | |
| 	if (!refer_tech) {
 | |
| 		ast_log(LOG_ERROR, "Unknown refer tech: %s\n", tech_name);
 | |
| 		ast_rwlock_unlock(&refer_techs_lock);
 | |
| 		ao2_ref(refer, -1);
 | |
| 		return -1;
 | |
| 	}
 | |
| 
 | |
| 	ao2_lock(refer);
 | |
| 	res = refer_tech->refer_send(refer);
 | |
| 	ao2_unlock(refer);
 | |
| 
 | |
| 	ast_rwlock_unlock(&refer_techs_lock);
 | |
| 
 | |
| 	ao2_ref(refer, -1);
 | |
| 
 | |
| 	return res;
 | |
| }
 | |
| 
 | |
| int ast_refer_tech_register(const struct ast_refer_tech *tech)
 | |
| {
 | |
| 	const struct ast_refer_tech *match;
 | |
| 
 | |
| 	ast_rwlock_wrlock(&refer_techs_lock);
 | |
| 
 | |
| 	match = refer_find_by_tech_name(tech->name);
 | |
| 	if (match) {
 | |
| 		ast_log(LOG_ERROR, "Refer technology already registered for '%s'\n",
 | |
| 		        tech->name);
 | |
| 		ast_rwlock_unlock(&refer_techs_lock);
 | |
| 		return -1;
 | |
| 	}
 | |
| 
 | |
| 	if (AST_VECTOR_APPEND(&refer_techs, tech)) {
 | |
| 		ast_log(LOG_ERROR, "Failed to register refer technology for '%s'\n",
 | |
| 		        tech->name);
 | |
| 		ast_rwlock_unlock(&refer_techs_lock);
 | |
| 		return -1;
 | |
| 	}
 | |
| 	ast_verb(5, "Refer technology '%s' registered.\n", tech->name);
 | |
| 
 | |
| 	ast_rwlock_unlock(&refer_techs_lock);
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| /*!
 | |
|  * \brief Comparison callback for \c ast_refer_tech vector removal
 | |
|  *
 | |
|  * \param vec_elem The element in the vector being compared
 | |
|  * \param srch The element being looked up
 | |
|  *
 | |
|  * \retval non-zero The items are equal
 | |
|  * \retval 0 The items are not equal
 | |
|  */
 | |
| static int refer_tech_cmp(const struct ast_refer_tech *vec_elem, const struct ast_refer_tech *srch)
 | |
| {
 | |
| 	if (!vec_elem->name || !srch->name) {
 | |
| 		return (vec_elem->name == srch->name) ? 1 : 0;
 | |
| 	}
 | |
| 	return !strcmp(vec_elem->name, srch->name);
 | |
| }
 | |
| 
 | |
| int ast_refer_tech_unregister(const struct ast_refer_tech *tech)
 | |
| {
 | |
| 	int match;
 | |
| 
 | |
| 	ast_rwlock_wrlock(&refer_techs_lock);
 | |
| 	match = AST_VECTOR_REMOVE_CMP_UNORDERED(&refer_techs, tech, refer_tech_cmp,
 | |
| 	                                        AST_VECTOR_ELEM_CLEANUP_NOOP);
 | |
| 	ast_rwlock_unlock(&refer_techs_lock);
 | |
| 
 | |
| 	if (match) {
 | |
| 		ast_log(LOG_ERROR, "No '%s' refer technology found.\n", tech->name);
 | |
| 		return -1;
 | |
| 	}
 | |
| 
 | |
| 	ast_verb(5, "Refer technology '%s' unregistered.\n", tech->name);
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| /*!
 | |
|  * \internal
 | |
|  * \brief Clean up other resources on Asterisk shutdown
 | |
|  */
 | |
| static void refer_shutdown(void)
 | |
| {
 | |
| 	AST_VECTOR_FREE(&refer_techs);
 | |
| 	ast_rwlock_destroy(&refer_techs_lock);
 | |
| }
 | |
| 
 | |
| /*!
 | |
|  * \internal
 | |
|  * \brief Initialize stuff during Asterisk startup.
 | |
|  *
 | |
|  * Cleanup isn't a big deal in this function.  If we return non-zero,
 | |
|  * Asterisk is going to exit.
 | |
|  *
 | |
|  * \retval 0 success
 | |
|  * \retval non-zero failure
 | |
|  */
 | |
| int ast_refer_init(void)
 | |
| {
 | |
| 	ast_rwlock_init(&refer_techs_lock);
 | |
| 	if (AST_VECTOR_INIT(&refer_techs, 8)) {
 | |
| 		return -1;
 | |
| 	}
 | |
| 	ast_register_cleanup(refer_shutdown);
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| int ast_refer_notify_transfer_request(struct ast_channel *source, const char *referred_by,
 | |
| 				const char *exten, const char *protocol_id,
 | |
| 				struct ast_channel *dest, struct ast_refer_params *params,
 | |
| 				enum ast_control_transfer state)
 | |
| {
 | |
| 	RAII_VAR(struct ast_ari_transfer_message *, transfer_message, NULL, ao2_cleanup);
 | |
| 	RAII_VAR(struct stasis_message *, msg, NULL, ao2_cleanup);
 | |
| 	RAII_VAR(struct ast_bridge *, source_bridge, NULL, ao2_cleanup);
 | |
| 	RAII_VAR(struct ast_bridge *, dest_bridge, NULL, ao2_cleanup);
 | |
| 
 | |
| 	transfer_message = ast_ari_transfer_message_create(source, referred_by, exten, protocol_id, dest, params, state);
 | |
| 	if (!transfer_message) {
 | |
| 		return -1;
 | |
| 	}
 | |
| 	source_bridge = ast_bridge_transfer_acquire_bridge(source);
 | |
| 	if (source_bridge) {
 | |
| 		RAII_VAR(struct ast_channel *, peer, NULL, ast_channel_cleanup);
 | |
| 
 | |
| 		ast_bridge_lock(source_bridge);
 | |
| 		transfer_message->source_bridge = ast_bridge_get_snapshot(source_bridge);
 | |
| 		peer = ast_bridge_peer_nolock(source_bridge, source);
 | |
| 		if (peer) {
 | |
| 			ast_channel_lock(peer);
 | |
| 			transfer_message->source_peer = ao2_bump(ast_channel_snapshot(peer));
 | |
| 			ast_channel_unlock(peer);
 | |
| 		}
 | |
| 		ast_bridge_unlock(source_bridge);
 | |
| 	}
 | |
| 
 | |
| 	if (dest) {
 | |
| 		dest_bridge = ast_bridge_transfer_acquire_bridge(dest);
 | |
| 		if (dest_bridge) {
 | |
| 			RAII_VAR(struct ast_channel *, peer, NULL, ast_channel_cleanup);
 | |
| 
 | |
| 			ast_bridge_lock(dest_bridge);
 | |
| 			transfer_message->dest_bridge = ast_bridge_get_snapshot(dest_bridge);
 | |
| 			peer = ast_bridge_peer_nolock(dest_bridge, dest);
 | |
| 			if (peer) {
 | |
| 				ast_channel_lock(peer);
 | |
| 				transfer_message->dest_peer = ao2_bump(ast_channel_snapshot(peer));
 | |
| 				ast_channel_unlock(peer);
 | |
| 			}
 | |
| 			ast_bridge_unlock(dest_bridge);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	msg = stasis_message_create(ast_channel_transfer_request_type(), transfer_message);
 | |
| 	if (msg) {
 | |
| 		ast_channel_lock(source);
 | |
| 		stasis_publish(ast_channel_topic(source), msg);
 | |
| 		ast_channel_unlock(source);
 | |
| 	}
 | |
| 
 | |
| 	return 0;
 | |
| }
 |