diff options
-rw-r--r-- | bridges/bridge_builtin_features.c | 417 | ||||
-rw-r--r-- | include/asterisk/bridging.h | 12 | ||||
-rw-r--r-- | include/asterisk/bridging_features.h | 4 | ||||
-rw-r--r-- | include/asterisk/bridging_internal.h | 88 | ||||
-rw-r--r-- | include/asterisk/bridging_roles.h | 41 | ||||
-rw-r--r-- | include/asterisk/features_config.h | 2 | ||||
-rw-r--r-- | include/asterisk/stasis_bridging.h | 23 | ||||
-rw-r--r-- | main/bridging.c | 67 | ||||
-rw-r--r-- | main/bridging_basic.c | 2642 | ||||
-rw-r--r-- | main/bridging_roles.c | 20 | ||||
-rw-r--r-- | main/cel.c | 3 | ||||
-rw-r--r-- | main/features.c | 12 | ||||
-rw-r--r-- | main/features_config.c | 17 | ||||
-rw-r--r-- | main/stasis_bridging.c | 45 |
14 files changed, 2896 insertions, 497 deletions
diff --git a/bridges/bridge_builtin_features.c b/bridges/bridge_builtin_features.c index f0c4c2437..87b74d4e2 100644 --- a/bridges/bridge_builtin_features.c +++ b/bridges/bridge_builtin_features.c @@ -54,421 +54,6 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include "asterisk/mixmonitor.h" #include "asterisk/audiohook.h" -/*! - * \brief Helper function that presents dialtone and grabs extension - * - * \retval 0 on success - * \retval -1 on failure - */ -static int grab_transfer(struct ast_channel *chan, char *exten, size_t exten_len, const char *context) -{ - int res; - int digit_timeout; - RAII_VAR(struct ast_features_xfer_config *, xfer_cfg, NULL, ao2_cleanup); - - ast_channel_lock(chan); - xfer_cfg = ast_get_chan_features_xfer_config(chan); - if (!xfer_cfg) { - ast_log(LOG_ERROR, "Unable to get transfer configuration\n"); - ast_channel_unlock(chan); - return -1; - } - digit_timeout = xfer_cfg->transferdigittimeout; - ast_channel_unlock(chan); - - /* Play the simple "transfer" prompt out and wait */ - res = ast_stream_and_wait(chan, "pbx-transfer", AST_DIGIT_ANY); - ast_stopstream(chan); - if (res < 0) { - /* Hangup or error */ - return -1; - } - if (res) { - /* Store the DTMF digit that interrupted playback of the file. */ - exten[0] = res; - } - - /* Drop to dialtone so they can enter the extension they want to transfer to */ - res = ast_app_dtget(chan, context, exten, exten_len, exten_len - 1, digit_timeout); - if (res < 0) { - /* Hangup or error */ - res = -1; - } else if (!res) { - /* 0 for invalid extension dialed. */ - if (ast_strlen_zero(exten)) { - ast_debug(1, "%s dialed no digits.\n", ast_channel_name(chan)); - } else { - ast_debug(1, "%s dialed '%s@%s' does not exist.\n", - ast_channel_name(chan), exten, context); - } - ast_stream_and_wait(chan, "pbx-invalid", AST_DIGIT_NONE); - res = -1; - } else { - /* Dialed extension is valid. */ - res = 0; - } - return res; -} - -static void copy_caller_data(struct ast_channel *dest, struct ast_channel *caller) -{ - ast_channel_lock_both(caller, dest); - ast_connected_line_copy_from_caller(ast_channel_connected(dest), ast_channel_caller(caller)); - ast_channel_inherit_variables(caller, dest); - ast_channel_datastore_inherit(caller, dest); - ast_channel_unlock(dest); - ast_channel_unlock(caller); -} - -/*! \brief Helper function that creates an outgoing channel and returns it immediately */ -static struct ast_channel *dial_transfer(struct ast_channel *caller, const char *exten, const char *context) -{ - char destination[AST_MAX_EXTENSION + AST_MAX_CONTEXT + 1]; - struct ast_channel *chan; - int cause; - - /* Fill the variable with the extension and context we want to call */ - snprintf(destination, sizeof(destination), "%s@%s", exten, context); - - /* Now we request a local channel to prepare to call the destination */ - chan = ast_request("Local", ast_channel_nativeformats(caller), caller, destination, - &cause); - if (!chan) { - return NULL; - } - - /* Who is transferring the call. */ - pbx_builtin_setvar_helper(chan, "TRANSFERERNAME", ast_channel_name(caller)); - - /* To work as an analog to BLINDTRANSFER */ - pbx_builtin_setvar_helper(chan, "ATTENDEDTRANSFER", ast_channel_name(caller)); - - /* Before we actually dial out let's inherit appropriate information. */ - copy_caller_data(chan, caller); - - /* Since the above worked fine now we actually call it and return the channel */ - if (ast_call(chan, destination, 0)) { - ast_hangup(chan); - return NULL; - } - - return chan; -} - -/*! - * \internal - * \brief Determine the transfer context to use. - * \since 12.0.0 - * - * \param transferer Channel initiating the transfer. - * \param context User supplied context if available. May be NULL. - * - * \return The context to use for the transfer. - */ -static const char *get_transfer_context(struct ast_channel *transferer, const char *context) -{ - if (!ast_strlen_zero(context)) { - return context; - } - context = pbx_builtin_getvar_helper(transferer, "TRANSFER_CONTEXT"); - if (!ast_strlen_zero(context)) { - return context; - } - context = ast_channel_macrocontext(transferer); - if (!ast_strlen_zero(context)) { - return context; - } - context = ast_channel_context(transferer); - if (!ast_strlen_zero(context)) { - return context; - } - return "default"; -} - -static void blind_transfer_cb(struct ast_channel *new_channel, void *user_data, - enum ast_transfer_type transfer_type) -{ - struct ast_channel *transferer_channel = user_data; - - if (transfer_type == AST_BRIDGE_TRANSFER_MULTI_PARTY) { - copy_caller_data(new_channel, transferer_channel); - } -} - -/*! \brief Internal built in feature for blind transfers */ -static int feature_blind_transfer(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel, void *hook_pvt) -{ - char exten[AST_MAX_EXTENSION] = ""; - struct ast_bridge_features_blind_transfer *blind_transfer = hook_pvt; - const char *context; - char *goto_on_blindxfr; - - ast_bridge_channel_write_hold(bridge_channel, NULL); - - ast_channel_lock(bridge_channel->chan); - context = ast_strdupa(get_transfer_context(bridge_channel->chan, - blind_transfer ? blind_transfer->context : NULL)); - goto_on_blindxfr = ast_strdupa(S_OR(pbx_builtin_getvar_helper(bridge_channel->chan, - "GOTO_ON_BLINDXFR"), "")); - ast_channel_unlock(bridge_channel->chan); - - /* Grab the extension to transfer to */ - if (grab_transfer(bridge_channel->chan, exten, sizeof(exten), context)) { - ast_bridge_channel_write_unhold(bridge_channel); - return 0; - } - - if (!ast_strlen_zero(goto_on_blindxfr)) { - ast_debug(1, "After transfer, transferer %s goes to %s\n", - ast_channel_name(bridge_channel->chan), goto_on_blindxfr); - ast_after_bridge_set_go_on(bridge_channel->chan, NULL, NULL, 0, goto_on_blindxfr); - } - - if (ast_bridge_transfer_blind(0, bridge_channel->chan, exten, context, blind_transfer_cb, - bridge_channel->chan) != AST_BRIDGE_TRANSFER_SUCCESS && - !ast_strlen_zero(goto_on_blindxfr)) { - ast_after_bridge_goto_discard(bridge_channel->chan); - } - - return 0; -} - -/*! Attended transfer code */ -enum atxfer_code { - /*! Party C hungup or other reason to abandon the transfer. */ - ATXFER_INCOMPLETE, - /*! Transfer party C to party A. */ - ATXFER_COMPLETE, - /*! Turn the transfer into a threeway call. */ - ATXFER_THREEWAY, - /*! Hangup party C and return party B to the bridge. */ - ATXFER_ABORT, -}; - -/*! \brief Attended transfer feature to complete transfer */ -static int attended_transfer_complete(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel, void *hook_pvt) -{ - enum atxfer_code *transfer_code = hook_pvt; - - *transfer_code = ATXFER_COMPLETE; - ast_bridge_change_state(bridge_channel, AST_BRIDGE_CHANNEL_STATE_HANGUP); - return 0; -} - -/*! \brief Attended transfer feature to turn it into a threeway call */ -static int attended_transfer_threeway(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel, void *hook_pvt) -{ - enum atxfer_code *transfer_code = hook_pvt; - - *transfer_code = ATXFER_THREEWAY; - ast_bridge_change_state(bridge_channel, AST_BRIDGE_CHANNEL_STATE_HANGUP); - return 0; -} - -/*! \brief Attended transfer feature to abort transfer */ -static int attended_transfer_abort(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel, void *hook_pvt) -{ - enum atxfer_code *transfer_code = hook_pvt; - - *transfer_code = ATXFER_ABORT; - ast_bridge_change_state(bridge_channel, AST_BRIDGE_CHANNEL_STATE_HANGUP); - return 0; -} - -/*! \brief Internal built in feature for attended transfers */ -static int feature_attended_transfer(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel, void *hook_pvt) -{ - char exten[AST_MAX_EXTENSION] = ""; - struct ast_channel *peer; - struct ast_bridge *attended_bridge; - struct ast_bridge_features caller_features; - int xfer_failed; - struct ast_bridge_features_attended_transfer *attended_transfer = hook_pvt; - const char *complete_sound; - const char *context; - enum atxfer_code transfer_code = ATXFER_INCOMPLETE; - const char *atxfer_abort; - const char *atxfer_threeway; - const char *atxfer_complete; - const char *fail_sound; - RAII_VAR(struct ast_features_xfer_config *, xfer_cfg, NULL, ao2_cleanup); - - ast_bridge_channel_write_hold(bridge_channel, NULL); - - bridge = ast_bridge_channel_merge_inhibit(bridge_channel, +1); - - ast_channel_lock(bridge_channel->chan); - context = ast_strdupa(get_transfer_context(bridge_channel->chan, - attended_transfer ? attended_transfer->context : NULL)); - xfer_cfg = ast_get_chan_features_xfer_config(bridge_channel->chan); - if (!xfer_cfg) { - ast_log(LOG_ERROR, "Unable to get transfer configuration options\n"); - ast_channel_unlock(bridge_channel->chan); - return 0; - } - if (attended_transfer) { - atxfer_abort = ast_strdupa(S_OR(attended_transfer->abort, xfer_cfg->atxferabort)); - atxfer_threeway = ast_strdupa(S_OR(attended_transfer->threeway, xfer_cfg->atxferthreeway)); - atxfer_complete = ast_strdupa(S_OR(attended_transfer->complete, xfer_cfg->atxfercomplete)); - } else { - atxfer_abort = ast_strdupa(xfer_cfg->atxferabort); - atxfer_threeway = ast_strdupa(xfer_cfg->atxferthreeway); - atxfer_complete = ast_strdupa(xfer_cfg->atxfercomplete); - } - fail_sound = ast_strdupa(xfer_cfg->xferfailsound); - ast_channel_unlock(bridge_channel->chan); - - /* Grab the extension to transfer to */ - if (grab_transfer(bridge_channel->chan, exten, sizeof(exten), context)) { - ast_bridge_merge_inhibit(bridge, -1); - ao2_ref(bridge, -1); - ast_bridge_channel_write_unhold(bridge_channel); - return 0; - } - - /* Get a channel that is the destination we wish to call */ - peer = dial_transfer(bridge_channel->chan, exten, context); - if (!peer) { - ast_bridge_merge_inhibit(bridge, -1); - ao2_ref(bridge, -1); - ast_stream_and_wait(bridge_channel->chan, fail_sound, AST_DIGIT_NONE); - ast_bridge_channel_write_unhold(bridge_channel); - return 0; - } - -/* BUGBUG bridging API features does not support the features.conf atxfer bounce between C & B channels */ - /* Setup a DTMF menu to control the transfer. */ - if (ast_bridge_features_init(&caller_features) - || ast_bridge_hangup_hook(&caller_features, - attended_transfer_complete, &transfer_code, NULL, 0) - || ast_bridge_dtmf_hook(&caller_features, atxfer_abort, - attended_transfer_abort, &transfer_code, NULL, 0) - || ast_bridge_dtmf_hook(&caller_features, atxfer_complete, - attended_transfer_complete, &transfer_code, NULL, 0) - || ast_bridge_dtmf_hook(&caller_features, atxfer_threeway, - attended_transfer_threeway, &transfer_code, NULL, 0)) { - ast_bridge_features_cleanup(&caller_features); - ast_hangup(peer); - ast_bridge_merge_inhibit(bridge, -1); - ao2_ref(bridge, -1); - ast_stream_and_wait(bridge_channel->chan, fail_sound, AST_DIGIT_NONE); - ast_bridge_channel_write_unhold(bridge_channel); - return 0; - } - - /* Create a bridge to use to talk to the person we are calling */ - attended_bridge = ast_bridge_base_new(AST_BRIDGE_CAPABILITY_1TO1MIX, - AST_BRIDGE_FLAG_DISSOLVE_HANGUP); - if (!attended_bridge) { - ast_bridge_features_cleanup(&caller_features); - ast_hangup(peer); - ast_bridge_merge_inhibit(bridge, -1); - ao2_ref(bridge, -1); - ast_stream_and_wait(bridge_channel->chan, fail_sound, AST_DIGIT_NONE); - ast_bridge_channel_write_unhold(bridge_channel); - return 0; - } - ast_bridge_merge_inhibit(attended_bridge, +1); - - /* This is how this is going down, we are imparting the channel we called above into this bridge first */ -/* BUGBUG we should impart the peer as an independent and move it to the original bridge. */ - if (ast_bridge_impart(attended_bridge, peer, NULL, NULL, 0)) { - ast_bridge_destroy(attended_bridge); - ast_bridge_features_cleanup(&caller_features); - ast_hangup(peer); - ast_bridge_merge_inhibit(bridge, -1); - ao2_ref(bridge, -1); - ast_stream_and_wait(bridge_channel->chan, fail_sound, AST_DIGIT_NONE); - ast_bridge_channel_write_unhold(bridge_channel); - return 0; - } - - /* - * For the caller we want to join the bridge in a blocking - * fashion so we don't spin around in this function doing - * nothing while waiting. - */ - ast_bridge_join(attended_bridge, bridge_channel->chan, NULL, &caller_features, NULL, 0); - -/* - * BUGBUG there is a small window where the channel does not point to the bridge_channel. - * - * This window is expected to go away when atxfer is redesigned - * to fully support existing functionality. There will be one - * and only one ast_bridge_channel structure per channel. - */ - /* Point the channel back to the original bridge and bridge_channel. */ - ast_bridge_channel_lock(bridge_channel); - ast_channel_lock(bridge_channel->chan); - ast_channel_internal_bridge_channel_set(bridge_channel->chan, bridge_channel); - ast_channel_internal_bridge_set(bridge_channel->chan, bridge_channel->bridge); - ast_channel_unlock(bridge_channel->chan); - ast_bridge_channel_unlock(bridge_channel); - - /* Wait for peer thread to exit bridge and die. */ - if (!ast_autoservice_start(bridge_channel->chan)) { - ast_bridge_depart(peer); - ast_autoservice_stop(bridge_channel->chan); - } else { - ast_bridge_depart(peer); - } - - /* Now that all channels are out of it we can destroy the bridge and the feature structures */ - ast_bridge_destroy(attended_bridge); - ast_bridge_features_cleanup(&caller_features); - - /* Is there a courtesy sound to play to the peer? */ - ast_channel_lock(bridge_channel->chan); - complete_sound = pbx_builtin_getvar_helper(bridge_channel->chan, - "ATTENDED_TRANSFER_COMPLETE_SOUND"); - if (!ast_strlen_zero(complete_sound)) { - complete_sound = ast_strdupa(complete_sound); - } else { - complete_sound = NULL; - } - ast_channel_unlock(bridge_channel->chan); - if (complete_sound) { - pbx_builtin_setvar_helper(peer, "BRIDGE_PLAY_SOUND", complete_sound); - } - - xfer_failed = -1; - switch (transfer_code) { - case ATXFER_INCOMPLETE: - /* Peer hungup */ - break; - case ATXFER_COMPLETE: - /* The peer takes our place in the bridge. */ - ast_bridge_channel_write_unhold(bridge_channel); - ast_bridge_change_state(bridge_channel, AST_BRIDGE_CHANNEL_STATE_HANGUP); - xfer_failed = ast_bridge_impart(bridge_channel->bridge, peer, bridge_channel->chan, NULL, 1); - break; - case ATXFER_THREEWAY: - /* - * Transferer wants to convert to a threeway call. - * - * Just impart the peer onto the bridge and have us return to it - * as normal. - */ - ast_bridge_channel_write_unhold(bridge_channel); - xfer_failed = ast_bridge_impart(bridge_channel->bridge, peer, NULL, NULL, 1); - break; - case ATXFER_ABORT: - /* Transferer decided not to transfer the call after all. */ - break; - } - ast_bridge_merge_inhibit(bridge, -1); - ao2_ref(bridge, -1); - if (xfer_failed) { - ast_hangup(peer); - if (!ast_check_hangup_locked(bridge_channel->chan)) { - ast_stream_and_wait(bridge_channel->chan, fail_sound, AST_DIGIT_NONE); - } - ast_bridge_channel_write_unhold(bridge_channel); - } - - return 0; -} - enum set_touch_variables_res { SET_TOUCH_SUCCESS, SET_TOUCH_UNSET, @@ -909,8 +494,6 @@ static int unload_module(void) static int load_module(void) { - ast_bridge_features_register(AST_BRIDGE_BUILTIN_BLINDTRANSFER, feature_blind_transfer, NULL); - ast_bridge_features_register(AST_BRIDGE_BUILTIN_ATTENDEDTRANSFER, feature_attended_transfer, NULL); ast_bridge_features_register(AST_BRIDGE_BUILTIN_HANGUP, feature_hangup, NULL); ast_bridge_features_register(AST_BRIDGE_BUILTIN_AUTOMON, feature_automonitor, NULL); ast_bridge_features_register(AST_BRIDGE_BUILTIN_AUTOMIXMON, feature_automixmonitor, NULL); diff --git a/include/asterisk/bridging.h b/include/asterisk/bridging.h index d770937a6..01565818b 100644 --- a/include/asterisk/bridging.h +++ b/include/asterisk/bridging.h @@ -396,6 +396,8 @@ struct ast_bridge_methods { struct ast_bridge { /*! Bridge virtual method table. */ const struct ast_bridge_methods *v_table; + /*! "Personality" currently exhibited by bridge subclass */ + void *personality; /*! Immutable bridge UUID. */ char uniqueid[AST_UUID_STR_LEN]; /*! Bridge technology that is handling the bridge */ @@ -1785,6 +1787,16 @@ struct ast_channel *ast_bridge_peer_nolock(struct ast_bridge *bridge, struct ast */ struct ast_channel *ast_bridge_peer(struct ast_bridge *bridge, struct ast_channel *chan); +/*! + * \brief Remove marked bridge channel feature hooks. + * \since 12.0.0 + * + * \param features Bridge features structure + * \param flags Determinator for whether hook is removed. + * + * \return Nothing + */ +void ast_bridge_features_remove(struct ast_bridge_features *features, enum ast_bridge_hook_remove_flags flags); #if defined(__cplusplus) || defined(c_plusplus) } #endif diff --git a/include/asterisk/bridging_features.h b/include/asterisk/bridging_features.h index d03219705..736441f6a 100644 --- a/include/asterisk/bridging_features.h +++ b/include/asterisk/bridging_features.h @@ -183,6 +183,8 @@ struct ast_bridge_hook_timer { enum ast_bridge_hook_remove_flags { /*! The hook is removed when the channel is pulled from the bridge. */ AST_BRIDGE_HOOK_REMOVE_ON_PULL = (1 << 0), + /*! The hook is removed when the bridge's personality changes. */ + AST_BRIDGE_HOOK_REMOVE_ON_PERSONALITY_CHANGE = (1 << 1), }; /* BUGBUG Need to be able to selectively remove DTMF, hangup, and interval hooks. */ @@ -265,6 +267,8 @@ struct ast_bridge_features_attended_transfer { char threeway[MAXIMUM_DTMF_FEATURE_STRING]; /*! DTMF string used to complete the transfer (If not empty.) */ char complete[MAXIMUM_DTMF_FEATURE_STRING]; + /*! DTMF string used to swap bridged targets (If not empty.) */ + char swap[MAXIMUM_DTMF_FEATURE_STRING]; }; enum ast_bridge_features_monitor { diff --git a/include/asterisk/bridging_internal.h b/include/asterisk/bridging_internal.h new file mode 100644 index 000000000..132c42b76 --- /dev/null +++ b/include/asterisk/bridging_internal.h @@ -0,0 +1,88 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013 Digium, Inc. + * + * Mark Michelson <mmichelson@digium.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 Private Bridging API + * + * \author Mark Michelson <mmichelson@digium.com> + * + * See Also: + * \arg \ref AstCREDITS + */ + +#ifndef _ASTERISK_PRIVATE_BRIDGING_H +#define _ASTERISK_PRIVATE_BRIDGING_H + +struct ast_bridge; +struct ast_bridge_channel; + +/*! + * \internal + * \brief Move a bridge channel from one bridge to another. + * \since 12.0.0 + * + * \param dst_bridge Destination bridge of bridge channel move. + * \param bridge_channel Channel moving from one bridge to another. + * \param attempt_recovery TRUE if failure attempts to push channel back into original bridge. + * \param optimized Indicates whether the move is part of an unreal channel optimization. + * + * \note The dst_bridge and bridge_channel->bridge are assumed already locked. + * + * \retval 0 on success. + * \retval -1 on failure. + */ +int bridge_move_do(struct ast_bridge *dst_bridge, struct ast_bridge_channel *bridge_channel, + int attempt_recovery, unsigned int optimized); + +/*! + * \internal + * \brief Do the merge of two bridges. + * \since 12.0.0 + * + * \param dst_bridge Destination bridge of merge. + * \param src_bridge Source bridge of merge. + * \param kick_me Array of channels to kick from the bridges. + * \param num_kick Number of channels in the kick_me array. + * \param optimized Indicates whether the merge is part of an unreal channel optimization. + * + * \return Nothing + * + * \note The two bridges are assumed already locked. + * + * This moves the channels in src_bridge into the bridge pointed + * to by dst_bridge. + */ +void bridge_merge_do(struct ast_bridge *dst_bridge, struct ast_bridge *src_bridge, + struct ast_bridge_channel **kick_me, unsigned int num_kick, unsigned int optimized); + +/*! + * \internal + * \brief Helper function to find a bridge channel given a channel. + * + * \param bridge What to search + * \param chan What to search for. + * + * \note On entry, bridge is already locked. + * + * \retval bridge_channel if channel is in the bridge. + * \retval NULL if not in bridge. + */ +struct ast_bridge_channel *find_bridge_channel(struct ast_bridge *bridge, struct ast_channel *chan); + +#endif /* _ASTERISK_PRIVATE_BRIDGING_H */ diff --git a/include/asterisk/bridging_roles.h b/include/asterisk/bridging_roles.h index 3acb67fae..d90e564b7 100644 --- a/include/asterisk/bridging_roles.h +++ b/include/asterisk/bridging_roles.h @@ -64,6 +64,37 @@ void ast_channel_remove_bridge_role(struct ast_channel *chan, const char *role_n int ast_channel_set_bridge_role_option(struct ast_channel *channel, const char *role_name, const char *option, const char *value); /*! + * \brief Check if a role exists on a channel + * + * \param channel The channel to check + * \param role_name The name of the role to search for + * + * \retval 0 The requested role does not exist on the channel + * \retval 1 The requested role exists on the channel + * + * This is an alternative to \ref ast_bridge_channel_has_role that is useful if bridge + * roles have not yet been established on a channel's bridge_channel. A possible example of + * when this could be used is in a bridge v_table's push() callback. + */ +int ast_channel_has_role(struct ast_channel *channel, const char *role_name); + +/*! + * \brief Retrieve the value of a requested role option from a channel + * + * \param channel The channel to retrieve the requested option from + * \param role_name The role to which the option belongs + * \param option The name of the option to retrieve + * + * \retval NULL The option does not exist + * \retval non-NULL The value of the option + * + * This is an alternative to \ref ast_bridge_channel_get_role_option that is useful if bridge + * roles have not yet been esstablished on a channel's bridge_channel. A possible example of + * when this could be used is in a bridge v_table's push() callback. + */ +const char *ast_channel_get_role_option(struct ast_channel *channel, const char *role_name, const char *option); + +/*! * \brief Check to see if a bridge channel inherited a specific role from its channel * * \param bridge_channel The bridge channel being checked @@ -72,7 +103,7 @@ int ast_channel_set_bridge_role_option(struct ast_channel *channel, const char * * \retval 0 The bridge channel does not have the requested role * \retval 1 The bridge channel does have the requested role * - * \note Before a bridge_channel can effectively check roles against a bridge, ast_bridge_roles_bridge_channel_establish_roles + * \note Before a bridge_channel can effectively check roles against a bridge, ast_bridge_channel_establish_roles * should be called on the bridge_channel so that roles and their respective role options can be copied from the channel * datastore into the bridge_channel roles list. Otherwise this function will just return 0 because the list will be NULL. */ @@ -88,10 +119,10 @@ int ast_bridge_channel_has_role(struct ast_bridge_channel *bridge_channel, const * \retval NULL If either the role does not exist on the bridge_channel or the role does exist but the option has not been set * \retval The value of the option * - * \note See ast_bridge_roles_channel_set_role_option note about the need to call ast_bridge_roles_bridge_channel_establish_roles. + * \note See ast_channel_set_role_option note about the need to call ast_bridge_channel_establish_roles. * * \note The returned character pointer is only valid as long as the bridge_channel is guaranteed to be alive and hasn't had - * ast_bridge_roles_bridge_channel_clear_roles called against it (as this will free all roles and role options in the bridge + * ast_bridge_channel_clear_roles called against it (as this will free all roles and role options in the bridge * channel). If you need this value after one of these destruction events occurs, you must make a local copy while it is * still valid. */ @@ -119,12 +150,12 @@ int ast_bridge_channel_establish_roles(struct ast_bridge_channel *bridge_channel * \param bridge_channel the bridge channel that we are scrubbing * * \details - * If roles are already established on a bridge channel, ast_bridge_roles_bridge_channel_establish_roles will fail unconditionally + * If roles are already established on a bridge channel, ast_bridge_channel_establish_roles will fail unconditionally * without changing any roles. In order to update a bridge channel's roles, they must first be cleared from the bridge channel using * this function. * * \note - * ast_bridge_roles_bridge_channel_clear_roles also serves as the destructor for the role list of a bridge channel. + * ast_bridge_channel_clear_roles also serves as the destructor for the role list of a bridge channel. */ void ast_bridge_channel_clear_roles(struct ast_bridge_channel *bridge_channel); diff --git a/include/asterisk/features_config.h b/include/asterisk/features_config.h index 7be019cd2..083496951 100644 --- a/include/asterisk/features_config.h +++ b/include/asterisk/features_config.h @@ -66,6 +66,8 @@ struct ast_features_xfer_config { AST_STRING_FIELD(atxfercomplete); /*! DTMF sequence used to turn an attempted atxfer into a three-way call */ AST_STRING_FIELD(atxferthreeway); + /*! DTMF sequence used to swap which party the transferer is talking to */ + AST_STRING_FIELD(atxferswap); ); /*! Milliseconds allowed between digit presses when dialing transfer destination */ unsigned int transferdigittimeout; diff --git a/include/asterisk/stasis_bridging.h b/include/asterisk/stasis_bridging.h index 606884fb2..336d7dd5d 100644 --- a/include/asterisk/stasis_bridging.h +++ b/include/asterisk/stasis_bridging.h @@ -256,6 +256,8 @@ enum ast_attended_transfer_dest_type { AST_ATTENDED_TRANSFER_DEST_APP, /*! The transfer results in both bridges remaining with a local channel linking them */ AST_ATTENDED_TRANSFER_DEST_LINK, + /*! The transfer results in a threeway call between transferer, transferee, and transfer target */ + AST_ATTENDED_TRANSFER_DEST_THREEWAY, }; /*! @@ -279,6 +281,8 @@ struct ast_attended_transfer_message { char app[AST_MAX_APP]; /*! Pair of local channels linking the bridges. Applicable for AST_ATTENDED_TRANSFER_DEST_LINK */ struct ast_channel_snapshot *links[2]; + /*! Transferer channel and bridge that survived the transition to a threeway call. Applicable for AST_ATTENDED_TRANSFER_DEST_THREEWAY */ + struct ast_bridge_channel_snapshot_pair threeway; } dest; }; @@ -330,6 +334,25 @@ void ast_bridge_publish_attended_transfer_bridge_merge(int is_external, enum ast /*! * \since 12 + * \brief Publish an attended transfer that results in a threeway call. + * + * Publish an \ref ast_attended_transfer_message with the dest_type set to + * \c AST_ATTENDED_TRANSFER_DEST_THREEWAY. Like with \ref ast_bridge_publish_attended_transfer_bridge_merge, + * this results from merging two bridges together. The difference is that a + * transferer channel survives the bridge merge + * + * \param is_external Indicates if the transfer was initiated externally + * \param result The result of the transfer. + * \param transferee The bridge between the transferer and transferees as well as the transferer channel from that bridge + * \param target The bridge between the transferer and transfer targets as well as the transferer channel from that bridge + * \param final_pair The bridge that the parties end up in, and the transferer channel that is in this bridge. + */ +void ast_bridge_publish_attended_transfer_threeway(int is_external, enum ast_transfer_result result, + struct ast_bridge_channel_pair *transferee, struct ast_bridge_channel_pair *target, + struct ast_bridge_channel_pair *final_pair); + +/*! + * \since 12 * \brief Publish an attended transfer that results in an application being run * * Publish an \ref ast_attended_transfer_message with the dest_type set to diff --git a/main/bridging.c b/main/bridging.c index 53368dc79..0419ab1a3 100644 --- a/main/bridging.c +++ b/main/bridging.c @@ -63,6 +63,7 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include "asterisk/core_local.h" #include "asterisk/core_unreal.h" #include "asterisk/features_config.h" +#include "asterisk/bridging_internal.h" /*! All bridges container. */ static struct ao2_container *bridges; @@ -77,7 +78,6 @@ static AST_RWLIST_HEAD_STATIC(bridge_technologies, ast_bridge_technology); static void cleanup_video_mode(struct ast_bridge *bridge); static int bridge_make_compatible(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel); -static void bridge_features_remove(struct ast_bridge_features *features, enum ast_bridge_hook_remove_flags remove_flags); /*! Default DTMF keys for built in features */ static char builtin_features_dtmf[AST_BRIDGE_BUILTIN_END][MAXIMUM_DTMF_FEATURE_STRING]; @@ -463,19 +463,7 @@ void ast_bridge_channel_restore_formats(struct ast_bridge_channel *bridge_channe } } -/*! - * \internal - * \brief Helper function to find a bridge channel given a channel. - * - * \param bridge What to search - * \param chan What to search for. - * - * \note On entry, bridge is already locked. - * - * \retval bridge_channel if channel is in the bridge. - * \retval NULL if not in bridge. - */ -static struct ast_bridge_channel *find_bridge_channel(struct ast_bridge *bridge, struct ast_channel *chan) +struct ast_bridge_channel *find_bridge_channel(struct ast_bridge *bridge, struct ast_channel *chan) { struct ast_bridge_channel *bridge_channel; @@ -757,7 +745,7 @@ static int bridge_channel_push(struct ast_bridge_channel *bridge_channel) || ast_bridge_channel_establish_roles(bridge_channel)) { ast_debug(1, "Bridge %s: pushing %p(%s) into bridge failed\n", bridge->uniqueid, bridge_channel, ast_channel_name(bridge_channel->chan)); - bridge_features_remove(bridge_channel->features, AST_BRIDGE_HOOK_REMOVE_ON_PULL); + ast_bridge_features_remove(bridge_channel->features, AST_BRIDGE_HOOK_REMOVE_ON_PULL); return -1; } bridge_channel->in_bridge = 1; @@ -1719,7 +1707,7 @@ static int bridge_base_push(struct ast_bridge *self, struct ast_bridge_channel * */ static void bridge_base_pull(struct ast_bridge *self, struct ast_bridge_channel *bridge_channel) { - bridge_features_remove(bridge_channel->features, AST_BRIDGE_HOOK_REMOVE_ON_PULL); + ast_bridge_features_remove(bridge_channel->features, AST_BRIDGE_HOOK_REMOVE_ON_PULL); } /*! @@ -4164,24 +4152,7 @@ static void bridge_channel_change_bridge(struct ast_bridge_channel *bridge_chann ao2_ref(old_bridge, -1); } -/*! - * \internal - * \brief Do the merge of two bridges. - * \since 12.0.0 - * - * \param dst_bridge Destination bridge of merge. - * \param src_bridge Source bridge of merge. - * \param kick_me Array of channels to kick from the bridges. - * \param num_kick Number of channels in the kick_me array. - * - * \return Nothing - * - * \note The two bridges are assumed already locked. - * - * This moves the channels in src_bridge into the bridge pointed - * to by dst_bridge. - */ -static void bridge_merge_do(struct ast_bridge *dst_bridge, struct ast_bridge *src_bridge, struct ast_bridge_channel **kick_me, unsigned int num_kick, +void bridge_merge_do(struct ast_bridge *dst_bridge, struct ast_bridge *src_bridge, struct ast_bridge_channel **kick_me, unsigned int num_kick, unsigned int optimized) { struct ast_bridge_channel *bridge_channel; @@ -4440,21 +4411,7 @@ int ast_bridge_merge(struct ast_bridge *dst_bridge, struct ast_bridge *src_bridg return res; } -/*! - * \internal - * \brief Move a bridge channel from one bridge to another. - * \since 12.0.0 - * - * \param dst_bridge Destination bridge of bridge channel move. - * \param bridge_channel Channel moving from one bridge to another. - * \param attempt_recovery TRUE if failure attempts to push channel back into original bridge. - * - * \note The dst_bridge and bridge_channel->bridge are assumed already locked. - * - * \retval 0 on success. - * \retval -1 on failure. - */ -static int bridge_move_do(struct ast_bridge *dst_bridge, struct ast_bridge_channel *bridge_channel, int attempt_recovery, +int bridge_move_do(struct ast_bridge *dst_bridge, struct ast_bridge_channel *bridge_channel, int attempt_recovery, unsigned int optimized) { struct ast_bridge *orig_bridge; @@ -5646,17 +5603,7 @@ static void hooks_remove_heap(struct ast_heap *hooks, enum ast_bridge_hook_remov ast_heap_unlock(hooks); } -/*! - * \internal - * \brief Remove marked bridge channel feature hooks. - * \since 12.0.0 - * - * \param features Bridge features structure - * \param remove_flags Determinator for whether hook is removed. - * - * \return Nothing - */ -static void bridge_features_remove(struct ast_bridge_features *features, enum ast_bridge_hook_remove_flags remove_flags) +void ast_bridge_features_remove(struct ast_bridge_features *features, enum ast_bridge_hook_remove_flags remove_flags) { hooks_remove_container(features->dtmf_hooks, remove_flags); hooks_remove_container(features->hangup_hooks, remove_flags); diff --git a/main/bridging_basic.c b/main/bridging_basic.c index 844ff3cbd..da4b4034a 100644 --- a/main/bridging_basic.c +++ b/main/bridging_basic.c @@ -37,6 +37,43 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include "asterisk/bridging.h" #include "asterisk/bridging_basic.h" #include "asterisk/astobj2.h" +#include "asterisk/features_config.h" +#include "asterisk/pbx.h" +#include "asterisk/file.h" +#include "asterisk/app.h" +#include "asterisk/bridging_internal.h" +#include "asterisk/dial.h" +#include "asterisk/stasis_bridging.h" + +#define NORMAL_FLAGS (AST_BRIDGE_FLAG_DISSOLVE_HANGUP | AST_BRIDGE_FLAG_DISSOLVE_EMPTY \ + | AST_BRIDGE_FLAG_SMART) + +#define TRANSFER_FLAGS AST_BRIDGE_FLAG_SMART +#define TRANSFERER_ROLE_NAME "transferer" + +struct attended_transfer_properties; + +enum bridge_basic_personality_type { + /*! Index for "normal" basic bridge personality */ + BRIDGE_BASIC_PERSONALITY_NORMAL, + /*! Index for attended transfer basic bridge personality */ + BRIDGE_BASIC_PERSONALITY_ATXFER, + /*! Indicates end of enum. Must always remain the last element */ + BRIDGE_BASIC_PERSONALITY_END, +}; + +/*! + * \brief Change basic bridge personality + * + * Changing personalities allows for the bridge to remain in use but have + * properties such as its v_table and its flags change. + * + * \param bridge The bridge + * \param type The personality to change the bridge to + * \user_data Private data to attach to the personality. + */ +static void bridge_basic_change_personality(struct ast_bridge *bridge, + enum bridge_basic_personality_type type, void *user_data); /* ------------------------------------------------------------------- */ @@ -117,6 +154,37 @@ static int basic_hangup_hook(struct ast_bridge *bridge, struct ast_bridge_channe } /*! + * \brief Details for specific basic bridge personalities + */ +struct personality_details { + /*! The v_table to use for this personality */ + struct ast_bridge_methods *v_table; + /*! Flags to set on this type of bridge */ + unsigned int bridge_flags; + /*! User data for this personality. If used, must be an ao2 object */ + void *pvt; + /*! Callback to be called when changing to the personality */ + void (*on_personality_change)(struct ast_bridge *bridge); +}; + +/*! + * \brief structure that organizes different personalities for basic bridges. + */ +struct bridge_basic_personality { + /*! The current bridge personality in use */ + enum bridge_basic_personality_type current; + /*! Array of details for the types of bridge personalities supported */ + struct personality_details details[BRIDGE_BASIC_PERSONALITY_END]; +}; + +static int add_normal_hooks(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel) +{ + return ast_bridge_hangup_hook(bridge_channel->features, basic_hangup_hook, + NULL, NULL, AST_BRIDGE_HOOK_REMOVE_ON_PULL) + || ast_bridge_channel_setup_features(bridge_channel); +} + +/*! * \internal * \brief ast_bridge basic push method. * \since 12.0.0 @@ -130,31 +198,2576 @@ static int basic_hangup_hook(struct ast_bridge *bridge, struct ast_bridge_channe * \retval 0 on success * \retval -1 on failure */ -static int bridge_basic_push(struct ast_bridge *self, struct ast_bridge_channel *bridge_channel, struct ast_bridge_channel *swap) +static int bridge_personality_normal_push(struct ast_bridge *self, struct ast_bridge_channel *bridge_channel, struct ast_bridge_channel *swap) { - if (ast_bridge_hangup_hook(bridge_channel->features, basic_hangup_hook, NULL, NULL, AST_BRIDGE_HOOK_REMOVE_ON_PULL) - || ast_bridge_channel_setup_features(bridge_channel)) { + if (add_normal_hooks(self, bridge_channel)) { return -1; } ast_bridge_update_accountcodes(self, bridge_channel, swap); ast_bridge_update_linkedids(self, bridge_channel, swap); + return 0; +} + +static int bridge_basic_push(struct ast_bridge *self, struct ast_bridge_channel *bridge_channel, struct ast_bridge_channel *swap) +{ + struct bridge_basic_personality *personality = self->personality; + + ast_assert(personality != NULL); + + if (personality->details[personality->current].v_table->push(self, bridge_channel, swap)) { + return -1; + } return ast_bridge_base_v_table.push(self, bridge_channel, swap); } +static void bridge_basic_pull(struct ast_bridge *self, struct ast_bridge_channel *bridge_channel) +{ + struct bridge_basic_personality *personality = self->personality; + + ast_assert(personality != NULL); + + if (personality->details[personality->current].v_table->pull) { + personality->details[personality->current].v_table->pull(self, bridge_channel); + } + + ast_bridge_base_v_table.pull(self, bridge_channel); +} + +static void bridge_basic_destroy(struct ast_bridge *self) +{ + struct bridge_basic_personality *personality = self->personality; + + ao2_cleanup(personality); + + ast_bridge_base_v_table.destroy(self); +} + +/*! + * \brief Remove appropriate hooks when basic bridge personality changes + * + * Hooks that have the AST_BRIDGE_HOOK_REMOVE_ON_PERSONALITY_CHANGE flag + * set will be removed from all bridge channels in the bridge. + * + * \param bridge Basic bridge undergoing personality change + */ +static void remove_hooks_on_personality_change(struct ast_bridge *bridge) +{ + struct ast_bridge_channel *iter; + + AST_LIST_TRAVERSE(&bridge->channels, iter, entry) { + SCOPED_LOCK(lock, iter, ast_bridge_channel_lock, ast_bridge_channel_unlock); + ast_bridge_features_remove(iter->features, AST_BRIDGE_HOOK_REMOVE_ON_PERSONALITY_CHANGE); + } +} + +/*! + * \brief Attended transfer superstates. + * + * An attended transfer's progress is facilitated by a state machine. + * The individual states of the state machine fall into the realm of + * one of two superstates. + */ +enum attended_transfer_superstate { + /*! + * \brief Transfer superstate + * + * The attended transfer state machine begins in this superstate. The + * goal of this state is for a transferer channel to facilitate a + * transfer from a transferee to a transfer target. + * + * There are two bridges used in this superstate. The transferee bridge is + * the bridge that the transferer and transferee channels originally + * communicate in, and the target bridge is the bridge where the transfer + * target is being dialed. + * + * The transferer channel is capable of moving between the bridges using + * the DTMF swap sequence. + */ + SUPERSTATE_TRANSFER, + /*! + * \brief Recall superstate + * + * The attended transfer state machine moves to this superstate if + * atxferdropcall is set to "no" and the transferer channel hangs up + * during a transfer. The goal in this superstate is to call back either + * the transfer target or transferer and rebridge with the transferee + * channel(s). + * + * In this superstate, there is only a single bridge used, the original + * transferee bridge. Rather than distinguishing between a transferer + * and transfer target, all outbound calls are toward a "recall_target" + * channel. + */ + SUPERSTATE_RECALL, +}; + +/*! + * The states in the attended transfer state machine. + */ +enum attended_transfer_state { + /*! + * \brief Calling Target state + * + * This state describes the initial state of a transfer. The transferer + * waits in the transfer target's bridge for the transfer target to answer. + * + * Superstate: Transfer + * + * Preconditions: + * 1) Transfer target is RINGING + * 2) Transferer is in transferee bridge + * 3) Transferee is on hold + * + * Transitions to TRANSFER_CALLING_TARGET: + * 1) This is the initial state for an attended transfer. + * 2) TRANSFER_HESITANT: Transferer presses DTMF swap sequence + * + * State operation: + * The transferer is moved from the transferee bridge into the transfer + * target bridge. + * + * Transitions from TRANSFER_CALLING_TARGET: + * 1) TRANSFER_FAIL: Transferee hangs up. + * 2) TRANSFER_BLOND: Transferer hangs up or presses DTMF swap sequence + * and configured atxferdropcall setting is yes. + * 3) TRANSFER_BLOND_NONFINAL: Transferer hangs up or presses DTMF swap + * sequence and configured atxferdroppcall setting is no. + * 4) TRANSFER_CONSULTING: Transfer target answers the call. + * 5) TRANSFER_REBRIDGE: Transfer target hangs up, call to transfer target + * times out, or transferer presses DTMF abort sequence. + * 6) TRANSFER_THREEWAY: Transferer presses DTMF threeway sequence. + * 7) TRANSFER_HESITANT: Transferer presses DTMF swap sequence. + */ + TRANSFER_CALLING_TARGET, + /*! + * \brief Hesitant state + * + * This state only arises if when waiting for the transfer target to + * answer, the transferer presses the DTMF swap sequence. This will + * cause the transferer to be rebridged with the transferee temporarily. + * + * Superstate: Transfer + * + * Preconditions: + * 1) Transfer target is in ringing state + * 2) Transferer is in transfer target bridge + * 3) Transferee is on hold + * + * Transitions to TRANSFER_HESITANT: + * 1) TRANSFER_CALLING_TARGET: Transferer presses DTMF swap sequence. + * + * State operation: + * The transferer is moved from the transfer target bridge into the + * transferee bridge, and the transferee is taken off hold. + * + * Transitions from TRANSFER_HESITANT: + * 1) TRANSFER_FAIL: Transferee hangs up + * 2) TRANSFER_BLOND: Transferer hangs up or presses DTMF swap sequence + * and configured atxferdropcall setting is yes. + * 3) TRANSFER_BLOND_NONFINAL: Transferer hangs up or presses DTMF swap + * sequence and configured atxferdroppcall setting is no. + * 4) TRANSFER_DOUBLECHECKING: Transfer target answers the call + * 5) TRANSFER_RESUME: Transfer target hangs up, call to transfer target + * times out, or transferer presses DTMF abort sequence. + * 6) TRANSFER_THREEWAY: Transferer presses DTMF threeway sequence. + * 7) TRANSFER_CALLING_TARGET: Transferer presses DTMF swap sequence. + */ + TRANSFER_HESITANT, + /*! + * \brief Rebridge state + * + * This is a terminal state that indicates that the transferer needs + * to move back to the transferee's bridge. This is a failed attended + * transfer result. + * + * Superstate: Transfer + * + * Preconditions: + * 1) Transferer is in transfer target bridge + * 2) Transferee is on hold + * + * Transitions to TRANSFER_REBRIDGE: + * 1) TRANSFER_CALLING_TARGET: Transfer target hangs up, call to transfer target + * times out, or transferer presses DTMF abort sequence. + * 2) TRANSFER_STATE_CONSULTING: Transfer target hangs up, or transferer presses + * DTMF abort sequence. + * + * State operation: + * The transferer channel is moved from the transfer target bridge to the + * transferee bridge. The transferee is taken off hold. A stasis transfer + * message is published indicating a failed attended transfer. + * + * Transitions from TRANSFER_REBRIDGE: + * None + */ + TRANSFER_REBRIDGE, + /*! + * \brief Resume state + * + * This is a terminal state that indicates that the party bridged with the + * transferee is the final party to be bridged with that transferee. This state + * may come about due to a successful recall or due to a failed transfer. + * + * Superstate: Transfer or Recall + * + * Preconditions: + * In Transfer Superstate: + * 1) Transferer is in transferee bridge + * 2) Transferee is not on hold + * In Recall Superstate: + * 1) The recall target is in the transferee bridge + * 2) Transferee is not on hold + * + * Transitions to TRANSFER_RESUME: + * TRANSFER_HESITANT: Transfer target hangs up, call to transfer target times out, + * or transferer presses DTMF abort sequence. + * TRANSFER_DOUBLECHECKING: Transfer target hangs up or transferer presses DTMF + * abort sequence. + * TRANSFER_BLOND_NONFINAL: Recall target answers + * TRANSFER_RECALLING: Recall target answers + * TRANSFER_RETRANSFER: Recall target answers + * + * State operations: + * None + * + * Transitions from TRANSFER_RESUME: + * None + */ + TRANSFER_RESUME, + /*! + * \brief Threeway state + * + * This state results when the transferer wishes to have all parties involved + * in a transfer to be in the same bridge together. + * + * Superstate: Transfer + * + * Preconditions: + * 1) Transfer target state is either RINGING or UP + * 2) Transferer is in either bridge + * 3) Transferee is not on hold + * + * Transitions to TRANSFER_THREEWAY: + * 1) TRANSFER_CALLING_TARGET: Transferer presses DTMF threeway sequence. + * 2) TRANSFER_HESITANT: Transferer presses DTMF threeway sequence. + * 3) TRANSFER_CONSULTING: Transferer presses DTMF threeway sequence. + * 4) TRANSFER_DOUBLECHECKING: Transferer presses DTMF threeway sequence. + * + * State operation: + * The transfer target bridge is merged into the transferee bridge. + * + * Transitions from TRANSFER_THREEWAY: + * None. + */ + TRANSFER_THREEWAY, + /*! + * \brief Consulting state + * + * This state describes the case where the transferer and transfer target + * are able to converse in the transfer target's bridge prior to completing + * the transfer. + * + * Superstate: Transfer + * + * Preconditions: + * 1) Transfer target is UP + * 2) Transferer is in target bridge + * 3) Transferee is on hold + * + * Transitions to TRANSFER_CONSULTING: + * 1) TRANSFER_CALLING_TARGET: Transfer target answers. + * 2) TRANSFER_DOUBLECHECKING: Transferer presses DTMF swap sequence. + * + * State operations: + * None. + * + * Transitions from TRANSFER_CONSULTING: + * TRANSFER_COMPLETE: Transferer hangs up or transferer presses DTMF complete sequence. + * TRANSFER_REBRIDGE: Transfer target hangs up or transferer presses DTMF abort sequence. + * TRANSFER_THREEWAY: Transferer presses DTMF threeway sequence. + * TRANSFER_DOUBLECHECKING: Transferer presses DTMF swap sequence. + */ + TRANSFER_CONSULTING, + /*! + * \brief Double-checking state + * + * This state describes the case where the transferer and transferee are + * able to converse in the transferee's bridge prior to completing the transfer. The + * difference between this and TRANSFER_HESITANT is that the transfer target is + * UP in this case. + * + * Superstate: Transfer + * + * Preconditions: + * 1) Transfer target is UP and on hold + * 2) Transferer is in transferee bridge + * 3) Transferee is off hold + * + * Transitions to TRANSFER_DOUBLECHECKING: + * 1) TRANSFER_HESITANT: Transfer target answers. + * 2) TRANSFER_CONSULTING: Transferer presses DTMF swap sequence. + * + * State operations: + * None. + * + * Transitions from TRANSFER_DOUBLECHECKING: + * 1) TRANSFER_FAIL: Transferee hangs up. + * 2) TRANSFER_COMPLETE: Transferer hangs up or presses DTMF complete sequence. + * 3) TRANSFER_RESUME: Transfer target hangs up or transferer presses DTMF abort sequence. + * 4) TRANSFER_THREEWAY: Transferer presses DTMF threeway sequence. + * 5) TRANSFER_CONSULTING: Transferer presses the DTMF swap sequence. + */ + TRANSFER_DOUBLECHECKING, + /*! + * \brief Complete state + * + * This is a terminal state where a transferer has successfully completed an attended + * transfer. This state's goal is to get the transfer target and transferee into + * the same bridge and the transferer off the call. + * + * Superstate: Transfer + * + * Preconditions: + * 1) Transfer target is UP and off hold. + * 2) Transferer is in either bridge. + * 3) Transferee is off hold. + * + * Transitions to TRANSFER_COMPLETE: + * 1) TRANSFER_CONSULTING: transferer hangs up or presses the DTMF complete sequence. + * 2) TRANSFER_DOUBLECHECKING: transferer hangs up or presses the DTMF complete sequence. + * + * State operation: + * The transfer target bridge is merged into the transferee bridge. The transferer + * channel is kicked out of the bridges as part of the merge. + * + * State operations: + * 1) Merge the transfer target bridge into the transferee bridge, + * excluding the transferer channel from the merge. + * 2) Publish a stasis transfer message. + * + * Exit operations: + * This is a terminal state, so there are no exit operations. + */ + TRANSFER_COMPLETE, + /*! + * \brief Blond state + * + * This is a terminal state where a transferer has completed an attended transfer prior + * to the transfer target answering. This state is only entered if atxferdropcall + * is set to 'yes'. This is considered to be a successful attended transfer. + * + * Superstate: Transfer + * + * Preconditions: + * 1) Transfer target is RINGING. + * 2) Transferer is in either bridge. + * 3) Transferee is off hold. + * + * Transitions to TRANSFER_BLOND: + * 1) TRANSFER_CALLING_TARGET: Transferer hangs up or presses the DTMF complete sequence. + * atxferdropcall is set to 'yes'. + * 2) TRANSFER_HESITANT: Transferer hangs up or presses the DTMF complete sequence. + * atxferdropcall is set to 'yes'. + * + * State operations: + * The transfer target bridge is merged into the transferee bridge. The transferer + * channel is kicked out of the bridges as part of the merge. A stasis transfer + * publication is sent indicating a successful transfer. + * + * Transitions from TRANSFER_BLOND: + * None + */ + TRANSFER_BLOND, + /*! + * \brief Blond non-final state + * + * This state is very similar to the TRANSFER_BLOND state, except that + * this state is entered when atxferdropcall is set to 'no'. This is the + * initial state of the Recall superstate, so state operations mainly involve + * moving to the Recall superstate. This means that the transfer target, that + * is currently ringing is now known as the recall target. + * + * Superstate: Recall + * + * Preconditions: + * 1) Recall target is RINGING. + * 2) Transferee is off hold. + * + * Transitions to TRANSFER_BLOND_NONFINAL: + * 1) TRANSFER_CALLING_TARGET: Transferer hangs up or presses the DTMF complete sequence. + * atxferdropcall is set to 'no'. + * 2) TRANSFER_HESITANT: Transferer hangs up or presses the DTMF complete sequence. + * atxferdropcall is set to 'no'. + * + * State operation: + * The superstate of the attended transfer is changed from Transfer to Recall. + * The transfer target bridge is merged into the transferee bridge. The transferer + * channel is kicked out of the bridges as part of the merge. + * + * Transitions from TRANSFER_BLOND_NONFINAL: + * 1) TRANSFER_FAIL: Transferee hangs up + * 2) TRANSFER_RESUME: Recall target answers + * 3) TRANSFER_RECALLING: Recall target hangs up or time expires. + */ + TRANSFER_BLOND_NONFINAL, + /*! + * \brief Recalling state + * + * This state is entered if the recall target from the TRANSFER_BLOND_NONFINAL + * or TRANSFER_RETRANSFER states hangs up or does not answer. The goal of this + * state is to call back the original transferer in an attempt to recover the + * original call. + * + * Superstate: Recall + * + * Preconditions: + * 1) Recall target is down. + * 2) Transferee is off hold. + * + * Transitions to TRANSFER_RECALLING: + * 1) TRANSFER_BLOND_NONFINAL: Recall target hangs up or time expires. + * 2) TRANSFER_RETRANSFER: Recall target hangs up or time expires. + * atxferloopdelay is non-zero. + * 3) TRANSFER_WAIT_TO_RECALL: Time expires. + * + * State operation: + * The original transferer becomes the recall target and is called using the Dialing API. + * Ringing is indicated to the transferee. + * + * Transitions from TRANSFER_RECALLING: + * 1) TRANSFER_FAIL: + * a) Transferee hangs up. + * b) Recall target hangs up or time expires, and number of recall attempts exceeds atxfercallbackretries + * 2) TRANSFER_WAIT_TO_RETRANSFER: Recall target hangs up or time expires. + * atxferloopdelay is non-zero. + * 3) TRANSFER_RETRANSFER: Recall target hangs up or time expires. + * atxferloopdelay is zero. + * 4) TRANSFER_RESUME: Recall target answers. + */ + TRANSFER_RECALLING, + /*! + * \brief Wait to Retransfer state + * + * This state is used simply to give a bit of breathing room between attempting + * to call back the original transferer and attempting to call back the original + * transfer target. The transferee hears music on hold during this state as an + * auditory clue that no one is currently being dialed. + * + * Superstate: Recall + * + * Preconditions: + * 1) Recall target is down. + * 2) Transferee is off hold. + * + * Transitions to TRANSFER_WAIT_TO_RETRANSFER: + * 1) TRANSFER_RECALLING: Recall target hangs up or time expires. + * atxferloopdelay is non-zero. + * + * State operation: + * The transferee is placed on hold. + * + * Transitions from TRANSFER_WAIT_TO_RETRANSFER: + * 1) TRANSFER_FAIL: Transferee hangs up. + * 2) TRANSFER_RETRANSFER: Time expires. + */ + TRANSFER_WAIT_TO_RETRANSFER, + /*! + * \brief Retransfer state + * + * This state is used in order to attempt to call back the original + * transfer target channel from the transfer. The transferee hears + * ringing during this state as an auditory cue that a party is being + * dialed. + * + * Superstate: Recall + * + * Preconditions: + * 1) Recall target is down. + * 2) Transferee is off hold. + * + * Transitions to TRANSFER_RETRANSFER: + * 1) TRANSFER_RECALLING: Recall target hangs up or time expires. + * atxferloopdelay is zero. + * 2) TRANSFER_WAIT_TO_RETRANSFER: Time expires. + * + * State operation: + * The original transfer target is requested and is set as the recall target. + * The recall target is called and placed into the transferee bridge. + * + * Transitions from TRANSFER_RETRANSFER: + * 1) TRANSFER_FAIL: Transferee hangs up. + * 2) TRANSFER_WAIT_TO_RECALL: Recall target hangs up or time expires. + * atxferloopdelay is non-zero. + * 3) TRANSFER_RECALLING: Recall target hangs up or time expires. + * atxferloopdelay is zero. + */ + TRANSFER_RETRANSFER, + /*! + * \brief Wait to recall state + * + * This state is used simply to give a bit of breathing room between attempting + * to call back the original transfer target and attempting to call back the + * original transferer. The transferee hears music on hold during this state as an + * auditory clue that no one is currently being dialed. + * + * Superstate: Recall + * + * Preconditions: + * 1) Recall target is down. + * 2) Transferee is off hold. + * + * Transitions to TRANSFER_WAIT_TO_RECALL: + * 1) TRANSFER_RETRANSFER: Recall target hangs up or time expires. + * atxferloopdelay is non-zero. + * + * State operation: + * Transferee is placed on hold. + * + * Transitions from TRANSFER_WAIT_TO_RECALL: + * 1) TRANSFER_FAIL: Transferee hangs up + * 2) TRANSFER_RECALLING: Time expires + */ + TRANSFER_WAIT_TO_RECALL, + /*! + * \brief Fail state + * + * This state indicates that something occurred during the transfer that + * makes a graceful completion impossible. The most common stimulus for this + * state is when the transferee hangs up. + * + * Superstate: Transfer and Recall + * + * Preconditions: + * None + * + * Transitions to TRANSFER_FAIL: + * 1) TRANSFER_CALLING_TARGET: Transferee hangs up. + * 2) TRANSFER_HESITANT: Transferee hangs up. + * 3) TRANSFER_DOUBLECHECKING: Transferee hangs up. + * 4) TRANSFER_BLOND_NONFINAL: Transferee hangs up. + * 5) TRANSFER_RECALLING: + * a) Transferee hangs up. + * b) Recall target hangs up or time expires, and number of + * recall attempts exceeds atxfercallbackretries. + * 6) TRANSFER_WAIT_TO_RETRANSFER: Transferee hangs up. + * 7) TRANSFER_RETRANSFER: Transferee hangs up. + * 8) TRANSFER_WAIT_TO_RECALL: Transferee hangs up. + * + * State operation: + * A transfer stasis publication is made indicating a failed transfer. + * The transferee bridge is destroyed. + * + * Transitions from TRANSFER_FAIL: + * None. + */ + TRANSFER_FAIL, +}; + +/*! + * \brief Stimuli that can cause transfer state changes + */ +enum attended_transfer_stimulus { + /*! No stimulus. This literally can never happen. */ + STIMULUS_NONE, + /*! All of the transferee channels have been hung up. */ + STIMULUS_TRANSFEREE_HANGUP, + /*! The transferer has hung up. */ + STIMULUS_TRANSFERER_HANGUP, + /*! The transfer target channel has hung up. */ + STIMULUS_TRANSFER_TARGET_HANGUP, + /*! The transfer target channel has answered. */ + STIMULUS_TRANSFER_TARGET_ANSWER, + /*! The recall target channel has hung up. */ + STIMULUS_RECALL_TARGET_HANGUP, + /*! The recall target channel has answered. */ + STIMULUS_RECALL_TARGET_ANSWER, + /*! The current state's timer has expired. */ + STIMULUS_TIMEOUT, + /*! The transferer pressed the abort DTMF sequence. */ + STIMULUS_DTMF_ATXFER_ABORT, + /*! The transferer pressed the complete DTMF sequence. */ + STIMULUS_DTMF_ATXFER_COMPLETE, + /*! The transferer pressed the three-way DTMF sequence. */ + STIMULUS_DTMF_ATXFER_THREEWAY, + /*! The transferer pressed the swap DTMF sequence. */ + STIMULUS_DTMF_ATXFER_SWAP, +}; + +/*! + * \brief String representations of the various stimuli + * + * Used for debugging purposes + */ +const char *stimulus_strs[] = { + [STIMULUS_NONE] = "None", + [STIMULUS_TRANSFEREE_HANGUP] = "Transferee Hangup", + [STIMULUS_TRANSFERER_HANGUP] = "Transferer Hangup", + [STIMULUS_TRANSFER_TARGET_HANGUP] = "Transfer Target Hangup", + [STIMULUS_TRANSFER_TARGET_ANSWER] = "Transfer Target Answer", + [STIMULUS_RECALL_TARGET_HANGUP] = "Recall Target Hangup", + [STIMULUS_RECALL_TARGET_ANSWER] = "Recall Target Answer", + [STIMULUS_TIMEOUT] = "Timeout", + [STIMULUS_DTMF_ATXFER_ABORT] = "DTMF Abort", + [STIMULUS_DTMF_ATXFER_COMPLETE] = "DTMF Complete", + [STIMULUS_DTMF_ATXFER_THREEWAY] = "DTMF Threeway", + [STIMULUS_DTMF_ATXFER_SWAP] = "DTMF Swap", +}; + +struct stimulus_list { + enum attended_transfer_stimulus stimulus; + AST_LIST_ENTRY(stimulus_list) next; +}; + +/*! + * \brief Collection of data related to an attended transfer attempt + */ +struct attended_transfer_properties { + AST_DECLARE_STRING_FIELDS ( + /*! Extension of transfer target */ + AST_STRING_FIELD(exten); + /*! Context of transfer target */ + AST_STRING_FIELD(context); + /*! Sound to play on failure */ + AST_STRING_FIELD(failsound); + /*! Sound to play when transfer completes */ + AST_STRING_FIELD(xfersound); + /*! The channel technology of the transferer channel */ + AST_STRING_FIELD(transferer_type); + /*! The transferer channel address */ + AST_STRING_FIELD(transferer_addr); + ); + /*! Condition used to synchronize when stimuli are reported to the monitor thread */ + ast_cond_t cond; + /*! The bridge where the transferee resides. This bridge is also the bridge that + * survives a successful attended transfer. + */ + struct ast_bridge *transferee_bridge; + /*! The bridge used to place an outbound call to the transfer target. This + * bridge is merged with the transferee_bridge on a successful transfer. + */ + struct ast_bridge *target_bridge; + /*! The party that performs the attended transfer. */ + struct ast_channel *transferer; + /*! The local channel dialed to reach the transfer target. */ + struct ast_channel *transfer_target; + /*! The party that is currently being recalled. Depending on + * the current state, this may be either the party that originally + * was the transferer or the original transfer target + */ + struct ast_channel *recall_target; + /*! The absolute starting time for running timers */ + struct timeval start; + AST_LIST_HEAD_NOLOCK(,stimulus_list) stimulus_queue; + /*! The current state of the attended transfer */ + enum attended_transfer_state state; + /*! The current superstate of the attended transfer */ + enum attended_transfer_superstate superstate; + /*! Configured atxferdropcall from features.conf */ + int atxferdropcall; + /*! Configured atxfercallbackretries from features.conf */ + int atxfercallbackretries; + /*! Configured atxferloopdelay from features.conf */ + int atxferloopdelay; + /*! Configured atxfernoanswertimeout from features.conf */ + int atxfernoanswertimeout; + /*! Count of the number of times that recalls have been attempted */ + int retry_attempts; + /*! Framehook ID for outbounc call to transfer target or recall target */ + int target_framehook_id; + /*! Dial structure used when recalling transferer channel */ + struct ast_dial *dial; + /*! The bridging features the transferer has available */ + struct ast_flags transferer_features; +}; + +static void attended_transfer_properties_destructor(void *obj) +{ + struct attended_transfer_properties *props = obj; + + ast_debug(1, "Destroy attended transfer properties %p\n", props); + + ao2_cleanup(props->target_bridge); + ao2_cleanup(props->transferee_bridge); + /* Use ast_channel_cleanup() instead of ast_channel_unref() for channels since they may be NULL */ + ast_channel_cleanup(props->transferer); + ast_channel_cleanup(props->transfer_target); + ast_channel_cleanup(props->recall_target); + ast_string_field_free_memory(props); + ast_cond_destroy(&props->cond); +} + +/*! + * \internal + * \brief Determine the transfer context to use. + * \since 12.0.0 + * + * \param transferer Channel initiating the transfer. + * \param context User supplied context if available. May be NULL. + * + * \return The context to use for the transfer. + */ +static const char *get_transfer_context(struct ast_channel *transferer, const char *context) +{ + if (!ast_strlen_zero(context)) { + return context; + } + context = pbx_builtin_getvar_helper(transferer, "TRANSFER_CONTEXT"); + if (!ast_strlen_zero(context)) { + return context; + } + context = ast_channel_macrocontext(transferer); + if (!ast_strlen_zero(context)) { + return context; + } + context = ast_channel_context(transferer); + if (!ast_strlen_zero(context)) { + return context; + } + return "default"; +} + +/*! + * \brief Allocate and initialize attended transfer properties + * + * \param transferee_bridge The bridge where the transfer was initiated + * \param transferer The channel performing the attended transfer + * \param context Suggestion for what context the transfer target extension can be found in + * + * \retval NULL Failure to allocate or initialize + * \retval non-NULL Newly allocated properties + */ +static struct attended_transfer_properties *attended_transfer_properties_alloc( + struct ast_bridge *transferee_bridge, struct ast_channel *transferer, + const char *context) +{ + struct attended_transfer_properties *props; + char *tech; + char *addr; + char *serial; + RAII_VAR(struct ast_features_xfer_config *, xfer_cfg, NULL, ao2_cleanup); + struct ast_flags *transferer_features; + + props = ao2_alloc(sizeof(*props), attended_transfer_properties_destructor); + if (!props || ast_string_field_init(props, 64)) { + return NULL; + } + + ast_cond_init(&props->cond, NULL); + + props->target_framehook_id = -1; + props->transferer = ast_channel_ref(transferer); + + ast_channel_lock(props->transferer); + xfer_cfg = ast_get_chan_features_xfer_config(props->transferer); + if (!xfer_cfg) { + ast_log(LOG_ERROR, "Unable to get transfer configuration from channel %s\n", ast_channel_name(props->transferer)); + ao2_ref(props, -1); + return NULL; + } + transferer_features = ast_bridge_features_ds_get(props->transferer); + if (transferer_features) { + props->transferer_features = *transferer_features; + } + props->atxferdropcall = xfer_cfg->atxferdropcall; + props->atxfercallbackretries = xfer_cfg->atxfercallbackretries; + props->atxfernoanswertimeout = xfer_cfg->atxfernoanswertimeout; + props->atxferloopdelay = xfer_cfg->atxferloopdelay; + ast_string_field_set(props, context, get_transfer_context(transferer, context)); + ast_string_field_set(props, failsound, xfer_cfg->xferfailsound); + ast_string_field_set(props, xfersound, xfer_cfg->xfersound); + + tech = ast_strdupa(ast_channel_name(props->transferer)); + addr = strchr(tech, '/'); + if (!addr) { + ast_log(LOG_ERROR, "Transferer channel name does not follow typical channel naming format (tech/address)\n"); + ast_channel_unref(props->transferer); + return NULL; + } + *addr++ = '\0'; + serial = strrchr(addr, '-'); + if (serial) { + *serial = '\0'; + } + ast_string_field_set(props, transferer_type, tech); + ast_string_field_set(props, transferer_addr, addr); + + ast_channel_unlock(props->transferer); + + ast_debug(1, "Allocated attended transfer properties %p for transfer from %s\n", + props, ast_channel_name(props->transferer)); + return props; +} + +/*! + * \brief Free backlog of stimuli in the queue + */ +static void clear_stimulus_queue(struct attended_transfer_properties *props) +{ + struct stimulus_list *list; + SCOPED_AO2LOCK(lock, props); + + while ((list = AST_LIST_REMOVE_HEAD(&props->stimulus_queue, next))) { + ast_free(list); + } +} + +/*! + * \brief Initiate shutdown of attended transfer properties + * + * Calling this indicates that the attended transfer properties are no longer needed + * because the transfer operation has concluded. + */ +static void attended_transfer_properties_shutdown(struct attended_transfer_properties *props) +{ + ast_debug(1, "Shutting down attended transfer %p\n", props); + + if (props->transferee_bridge) { + ast_bridge_merge_inhibit(props->transferee_bridge, -1); + bridge_basic_change_personality(props->transferee_bridge, + BRIDGE_BASIC_PERSONALITY_NORMAL, NULL); + } + + if (props->target_bridge) { + ast_bridge_destroy(props->target_bridge); + props->target_bridge = NULL; + } + + if (props->transferer) { + ast_channel_remove_bridge_role(props->transferer, TRANSFERER_ROLE_NAME); + } + + clear_stimulus_queue(props); + + ao2_cleanup(props); +} + +static void stimulate_attended_transfer(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus) +{ + struct stimulus_list *list; + + list = ast_calloc(1, sizeof(*list)); + if (!list) { + ast_log(LOG_ERROR, "Unable to push event to attended transfer queue. Expect transfer to fail\n"); + return; + } + + list->stimulus = stimulus; + ao2_lock(props); + AST_LIST_INSERT_TAIL(&props->stimulus_queue, list, next); + ast_cond_signal(&props->cond); + ao2_unlock(props); +} + +/*! + * \brief Send a stasis publication for a successful attended transfer + */ +static void publish_transfer_success(struct attended_transfer_properties *props) +{ + struct ast_bridge_channel_pair transferee = { + .channel = props->transferer, + .bridge = props->transferee_bridge, + }; + struct ast_bridge_channel_pair transfer_target = { + .channel = props->transferer, + .bridge = props->target_bridge, + }; + + ast_bridge_publish_attended_transfer_bridge_merge(0, AST_BRIDGE_TRANSFER_SUCCESS, + &transferee, &transfer_target, props->transferee_bridge); +} + +/*! + * \brief Send a stasis publication for an attended transfer that ends in a threeway call + */ +static void publish_transfer_threeway(struct attended_transfer_properties *props) +{ + struct ast_bridge_channel_pair transferee = { + .channel = props->transferer, + .bridge = props->transferee_bridge, + }; + struct ast_bridge_channel_pair transfer_target = { + .channel = props->transferer, + .bridge = props->target_bridge, + }; + struct ast_bridge_channel_pair threeway = { + .channel = props->transferer, + .bridge = props->transferee_bridge, + }; + + ast_bridge_publish_attended_transfer_threeway(0, AST_BRIDGE_TRANSFER_SUCCESS, + &transferee, &transfer_target, &threeway); +} + +/*! + * \brief Send a stasis publication for a failed attended transfer + */ +static void publish_transfer_fail(struct attended_transfer_properties *props) +{ + struct ast_bridge_channel_pair transferee = { + .channel = props->transferer, + .bridge = props->transferee_bridge, + }; + struct ast_bridge_channel_pair transfer_target = { + .channel = props->transferer, + .bridge = props->target_bridge, + }; + + ast_bridge_publish_attended_transfer_fail(0, AST_BRIDGE_TRANSFER_FAIL, + &transferee, &transfer_target); +} + +/*! + * \brief Helper method to play a sound on a channel in a bridge + * + * \param chan The channel to play the sound to + * \param sound The sound to play + */ +static void play_sound(struct ast_channel *chan, const char *sound) +{ + RAII_VAR(struct ast_bridge_channel *, bridge_channel, NULL, ao2_cleanup); + + ast_channel_lock(chan); + bridge_channel = ast_channel_get_bridge_channel(chan); + ast_channel_unlock(chan); + + if (!bridge_channel) { + return; + } + + ast_bridge_channel_queue_playfile(bridge_channel, NULL, sound, NULL); +} + +/*! + * \brief Helper method to place a channel in a bridge on hold + */ +static void hold(struct ast_channel *chan) +{ + RAII_VAR(struct ast_bridge_channel *, bridge_channel, NULL, ao2_cleanup); + + if (chan) { + ast_channel_lock(chan); + bridge_channel = ast_channel_get_bridge_channel(chan); + ast_channel_unlock(chan); + + ast_assert(bridge_channel != NULL); + + ast_bridge_channel_write_hold(bridge_channel, NULL); + } +} + +/*! + * \brief Helper method to take a channel in a bridge off hold + */ +static void unhold(struct ast_channel *chan) +{ + RAII_VAR(struct ast_bridge_channel *, bridge_channel, NULL, ao2_cleanup); + + ast_channel_lock(chan); + bridge_channel = ast_channel_get_bridge_channel(chan); + ast_channel_unlock(chan); + + ast_assert(bridge_channel != NULL); + + ast_bridge_channel_write_unhold(bridge_channel); +} + +/*! + * \brief Helper method to send a ringing indication to a channel in a bridge + */ +static void ringing(struct ast_channel *chan) +{ + RAII_VAR(struct ast_bridge_channel *, bridge_channel, NULL, ao2_cleanup); + + ast_channel_lock(chan); + bridge_channel = ast_channel_get_bridge_channel(chan); + ast_channel_unlock(chan); + + ast_assert(bridge_channel != NULL); + + ast_bridge_channel_write_control_data(bridge_channel, AST_CONTROL_RINGING, NULL, 0); +} + +/*! + * \brief Helper method to send a ringing indication to all channels in a bridge + */ +static void bridge_ringing(struct ast_bridge *bridge) +{ + struct ast_frame ringing = { + .frametype = AST_FRAME_CONTROL, + .subclass.integer = AST_CONTROL_RINGING, + }; + + ast_bridge_queue_everyone_else(bridge, NULL, &ringing); +} + +/*! + * \brief Helper method to send a hold frame to all channels in a bridge + */ +static void bridge_hold(struct ast_bridge *bridge) +{ + struct ast_frame hold = { + .frametype = AST_FRAME_CONTROL, + .subclass.integer = AST_CONTROL_HOLD, + }; + + ast_bridge_queue_everyone_else(bridge, NULL, &hold); +} + +/*! + * \brief Helper method to send an unhold frame to all channels in a bridge + */ +static void bridge_unhold(struct ast_bridge *bridge) +{ + struct ast_frame unhold = { + .frametype = AST_FRAME_CONTROL, + .subclass.integer = AST_CONTROL_UNHOLD, + }; + + ast_bridge_queue_everyone_else(bridge, NULL, &unhold); +} + +/*! + * \brief Wrapper for \ref bridge_move_do + */ +static int bridge_move(struct ast_bridge *dest, struct ast_bridge *src, struct ast_channel *channel, struct ast_channel *swap) +{ + int res; + RAII_VAR(struct ast_bridge_channel *, bridge_channel, NULL, ao2_cleanup); + + ast_bridge_lock_both(src, dest); + + ast_channel_lock(channel); + bridge_channel = ast_channel_get_bridge_channel(channel); + ast_channel_unlock(channel); + + ast_assert(bridge_channel != NULL); + + ao2_lock(bridge_channel); + bridge_channel->swap = swap; + ao2_unlock(bridge_channel); + + res = bridge_move_do(dest, bridge_channel, 1, 0); + + ast_bridge_unlock(dest); + ast_bridge_unlock(src); + + return res; +} + +/*! + * \brief Wrapper for \ref bridge_merge_do + */ +static void bridge_merge(struct ast_bridge *dest, struct ast_bridge *src, struct ast_channel **kick_channels, unsigned int num_channels) +{ + struct ast_bridge_channel **kick_bridge_channels = num_channels ? + ast_alloca(num_channels * sizeof(*kick_bridge_channels)) : NULL; + int i; + int num_bridge_channels = 0; + + ast_bridge_lock_both(dest, src); + + for (i = 0; i < num_channels; ++i) { + struct ast_bridge_channel *kick_bridge_channel; + + kick_bridge_channel = find_bridge_channel(src, kick_channels[i]); + if (!kick_bridge_channel) { + kick_bridge_channel = find_bridge_channel(dest, kick_channels[i]); + } + + /* It's possible (and fine) for the bridge channel to be NULL at this point if the + * channel has hung up already. If that happens, we can just remove it from the list + * of bridge channels to kick from the bridge + */ + if (!kick_bridge_channel) { + continue; + } + + kick_bridge_channels[num_bridge_channels++] = kick_bridge_channel; + } + + bridge_merge_do(dest, src, kick_bridge_channels, num_bridge_channels, 0); + ast_bridge_unlock(dest); + ast_bridge_unlock(src); +} + +/*! + * \brief Flags that indicate properties of attended transfer states + */ +enum attended_transfer_state_flags { + /*! This state requires that the timer be reset when entering the state */ + TRANSFER_STATE_FLAG_TIMER_RESET = (1 << 0), + /*! This state's timer uses atxferloopdelay */ + TRANSFER_STATE_FLAG_TIMER_LOOP_DELAY = (1 << 1), + /*! This state's timer uses atxfernoanswertimeout */ + TRANSFER_STATE_FLAG_ATXFER_NO_ANSWER = (1 << 2), + /*! This state has a time limit associated with it */ + TRANSFER_STATE_FLAG_TIMED = (TRANSFER_STATE_FLAG_TIMER_RESET | + TRANSFER_STATE_FLAG_TIMER_LOOP_DELAY | TRANSFER_STATE_FLAG_ATXFER_NO_ANSWER), + /*! This state does not transition to any other states */ + TRANSFER_STATE_FLAG_TERMINAL = (1 << 3), +}; + +static int calling_target_enter(struct attended_transfer_properties *props); +static enum attended_transfer_state calling_target_exit(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus); + +static int hesitant_enter(struct attended_transfer_properties *props); +static enum attended_transfer_state hesitant_exit(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus); + +static int rebridge_enter(struct attended_transfer_properties *props); + +static int resume_enter(struct attended_transfer_properties *props); + +static int threeway_enter(struct attended_transfer_properties *props); + +static int consulting_enter(struct attended_transfer_properties *props); +static enum attended_transfer_state consulting_exit(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus); + +static int double_checking_enter(struct attended_transfer_properties *props); +static enum attended_transfer_state double_checking_exit(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus); + +static int complete_enter(struct attended_transfer_properties *props); + +static int blond_enter(struct attended_transfer_properties *props); + +static int blond_nonfinal_enter(struct attended_transfer_properties *props); +static enum attended_transfer_state blond_nonfinal_exit(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus); + +static int recalling_enter(struct attended_transfer_properties *props); +static enum attended_transfer_state recalling_exit(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus); + +static int wait_to_retransfer_enter(struct attended_transfer_properties *props); +static enum attended_transfer_state wait_to_retransfer_exit(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus); + +static int retransfer_enter(struct attended_transfer_properties *props); +static enum attended_transfer_state retransfer_exit(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus); + +static int wait_to_recall_enter(struct attended_transfer_properties *props); +static enum attended_transfer_state wait_to_recall_exit(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus); + +static int fail_enter(struct attended_transfer_properties *props); + +/*! + * \brief Properties of an attended transfer state + */ +struct attended_transfer_state_properties { + /*! The name of the state. Used for debugging */ + const char *state_name; + /*! Function used to enter a state */ + int (*enter)(struct attended_transfer_properties *props); + /*! + * Function used to exit a state + * This is used both to determine what the next state + * to transition to will be and to perform any cleanup + * necessary before exiting the current state. + */ + enum attended_transfer_state (*exit)(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus); + /*! Flags associated with this state */ + enum attended_transfer_state_flags flags; +}; + +static const struct attended_transfer_state_properties state_properties[] = { + [TRANSFER_CALLING_TARGET] = { + .state_name = "Calling Target", + .enter = calling_target_enter, + .exit = calling_target_exit, + .flags = TRANSFER_STATE_FLAG_ATXFER_NO_ANSWER | TRANSFER_STATE_FLAG_TIMER_RESET, + }, + [TRANSFER_HESITANT] = { + .state_name = "Hesitant", + .enter = hesitant_enter, + .exit = hesitant_exit, + .flags = TRANSFER_STATE_FLAG_ATXFER_NO_ANSWER, + }, + [TRANSFER_REBRIDGE] = { + .state_name = "Rebridge", + .enter = rebridge_enter, + .flags = TRANSFER_STATE_FLAG_TERMINAL, + }, + [TRANSFER_RESUME] = { + .state_name = "Resume", + .enter = resume_enter, + .flags = TRANSFER_STATE_FLAG_TERMINAL, + }, + [TRANSFER_THREEWAY] = { + .state_name = "Threeway", + .enter = threeway_enter, + .flags = TRANSFER_STATE_FLAG_TERMINAL, + }, + [TRANSFER_CONSULTING] = { + .state_name = "Consulting", + .enter = consulting_enter, + .exit = consulting_exit, + }, + [TRANSFER_DOUBLECHECKING] = { + .state_name = "Double Checking", + .enter = double_checking_enter, + .exit = double_checking_exit, + }, + [TRANSFER_COMPLETE] = { + .state_name = "Complete", + .enter = complete_enter, + .flags = TRANSFER_STATE_FLAG_TERMINAL, + }, + [TRANSFER_BLOND] = { + .state_name = "Blond", + .enter = blond_enter, + .flags = TRANSFER_STATE_FLAG_TERMINAL, + }, + [TRANSFER_BLOND_NONFINAL] = { + .state_name = "Blond Non-Final", + .enter = blond_nonfinal_enter, + .exit = blond_nonfinal_exit, + .flags = TRANSFER_STATE_FLAG_ATXFER_NO_ANSWER, + }, + [TRANSFER_RECALLING] = { + .state_name = "Recalling", + .enter = recalling_enter, + .exit = recalling_exit, + .flags = TRANSFER_STATE_FLAG_ATXFER_NO_ANSWER | TRANSFER_STATE_FLAG_TIMER_RESET, + }, + [TRANSFER_WAIT_TO_RETRANSFER] = { + .state_name = "Wait to Retransfer", + .enter = wait_to_retransfer_enter, + .exit = wait_to_retransfer_exit, + .flags = TRANSFER_STATE_FLAG_TIMER_RESET | TRANSFER_STATE_FLAG_TIMER_LOOP_DELAY, + }, + [TRANSFER_RETRANSFER] = { + .state_name = "Retransfer", + .enter = retransfer_enter, + .exit = retransfer_exit, + .flags = TRANSFER_STATE_FLAG_ATXFER_NO_ANSWER | TRANSFER_STATE_FLAG_TIMER_RESET, + }, + [TRANSFER_WAIT_TO_RECALL] = { + .state_name = "Wait to Recall", + .enter = wait_to_recall_enter, + .exit = wait_to_recall_exit, + .flags = TRANSFER_STATE_FLAG_TIMER_RESET | TRANSFER_STATE_FLAG_TIMER_LOOP_DELAY, + }, + [TRANSFER_FAIL] = { + .state_name = "Fail", + .enter = fail_enter, + .flags = TRANSFER_STATE_FLAG_TERMINAL, + }, +}; + +static int calling_target_enter(struct attended_transfer_properties *props) +{ + return bridge_move(props->target_bridge, props->transferee_bridge, props->transferer, NULL); +} + +static enum attended_transfer_state calling_target_exit(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus) +{ + switch (stimulus) { + case STIMULUS_TRANSFEREE_HANGUP: + play_sound(props->transferer, props->failsound); + publish_transfer_fail(props); + return TRANSFER_FAIL; + case STIMULUS_DTMF_ATXFER_COMPLETE: + case STIMULUS_TRANSFERER_HANGUP: + bridge_unhold(props->transferee_bridge); + return props->atxferdropcall ? TRANSFER_BLOND : TRANSFER_BLOND_NONFINAL; + case STIMULUS_TRANSFER_TARGET_ANSWER: + return TRANSFER_CONSULTING; + case STIMULUS_TRANSFER_TARGET_HANGUP: + case STIMULUS_TIMEOUT: + case STIMULUS_DTMF_ATXFER_ABORT: + play_sound(props->transferer, props->failsound); + return TRANSFER_REBRIDGE; + case STIMULUS_DTMF_ATXFER_THREEWAY: + bridge_unhold(props->transferee_bridge); + return TRANSFER_THREEWAY; + case STIMULUS_DTMF_ATXFER_SWAP: + return TRANSFER_HESITANT; + case STIMULUS_NONE: + case STIMULUS_RECALL_TARGET_ANSWER: + case STIMULUS_RECALL_TARGET_HANGUP: + default: + ast_log(LOG_WARNING, "Unexpected stimulus '%s' received in attended transfer state '%s'\n", + stimulus_strs[stimulus], state_properties[props->state].state_name); + return props->state; + } +} + +static int hesitant_enter(struct attended_transfer_properties *props) +{ + if (bridge_move(props->transferee_bridge, props->target_bridge, props->transferer, NULL)) { + return -1; + } + + unhold(props->transferer); + return 0; +} + +static enum attended_transfer_state hesitant_exit(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus) +{ + switch (stimulus) { + case STIMULUS_TRANSFEREE_HANGUP: + play_sound(props->transferer, props->failsound); + publish_transfer_fail(props); + return TRANSFER_FAIL; + case STIMULUS_DTMF_ATXFER_COMPLETE: + case STIMULUS_TRANSFERER_HANGUP: + return props->atxferdropcall ? TRANSFER_BLOND : TRANSFER_BLOND_NONFINAL; + case STIMULUS_TRANSFER_TARGET_ANSWER: + return TRANSFER_DOUBLECHECKING; + case STIMULUS_TRANSFER_TARGET_HANGUP: + case STIMULUS_TIMEOUT: + case STIMULUS_DTMF_ATXFER_ABORT: + play_sound(props->transferer, props->failsound); + return TRANSFER_RESUME; + case STIMULUS_DTMF_ATXFER_THREEWAY: + return TRANSFER_THREEWAY; + case STIMULUS_DTMF_ATXFER_SWAP: + hold(props->transferer); + return TRANSFER_CALLING_TARGET; + case STIMULUS_NONE: + case STIMULUS_RECALL_TARGET_HANGUP: + case STIMULUS_RECALL_TARGET_ANSWER: + default: + ast_log(LOG_WARNING, "Unexpected stimulus '%s' received in attended transfer state '%s'\n", + stimulus_strs[stimulus], state_properties[props->state].state_name); + return props->state; + } +} + +static int rebridge_enter(struct attended_transfer_properties *props) +{ + if (bridge_move(props->transferee_bridge, props->target_bridge, + props->transferer, NULL)) { + return -1; + } + + unhold(props->transferer); + return 0; +} + +static int resume_enter(struct attended_transfer_properties *props) +{ + return 0; +} + +static int threeway_enter(struct attended_transfer_properties *props) +{ + bridge_merge(props->transferee_bridge, props->target_bridge, NULL, 0); + play_sound(props->transfer_target, props->xfersound); + play_sound(props->transferer, props->xfersound); + publish_transfer_threeway(props); + + return 0; +} + +static int consulting_enter(struct attended_transfer_properties *props) +{ + return 0; +} + +static enum attended_transfer_state consulting_exit(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus) +{ + switch (stimulus) { + case STIMULUS_TRANSFEREE_HANGUP: + /* This is a one-of-a-kind event. The transferer and transfer target are talking in + * one bridge, and the transferee has hung up in a separate bridge. In this case, we + * will change the personality of the transfer target bridge back to normal, and play + * a sound to the transferer to indicate the transferee is gone. + */ + bridge_basic_change_personality(props->target_bridge, BRIDGE_BASIC_PERSONALITY_NORMAL, NULL); + play_sound(props->transferer, props->failsound); + ast_bridge_merge_inhibit(props->target_bridge, -1); + /* These next two lines are here to ensure that our reference to the target bridge + * is cleaned up properly and that the target bridge is not destroyed when the + * monitor thread exits + */ + ao2_ref(props->target_bridge, -1); + props->target_bridge = NULL; + return TRANSFER_FAIL; + case STIMULUS_TRANSFERER_HANGUP: + case STIMULUS_DTMF_ATXFER_COMPLETE: + /* We know the transferer is in the target_bridge, so take the other bridge off hold */ + bridge_unhold(props->transferee_bridge); + return TRANSFER_COMPLETE; + case STIMULUS_TRANSFER_TARGET_HANGUP: + case STIMULUS_DTMF_ATXFER_ABORT: + play_sound(props->transferer, props->failsound); + return TRANSFER_REBRIDGE; + case STIMULUS_DTMF_ATXFER_THREEWAY: + bridge_unhold(props->transferee_bridge); + return TRANSFER_THREEWAY; + case STIMULUS_DTMF_ATXFER_SWAP: + hold(props->transferer); + bridge_move(props->transferee_bridge, props->target_bridge, props->transferer, NULL); + unhold(props->transferer); + return TRANSFER_DOUBLECHECKING; + case STIMULUS_NONE: + case STIMULUS_TIMEOUT: + case STIMULUS_TRANSFER_TARGET_ANSWER: + case STIMULUS_RECALL_TARGET_HANGUP: + case STIMULUS_RECALL_TARGET_ANSWER: + default: + ast_log(LOG_WARNING, "Unexpected stimulus '%s' received in attended transfer state '%s'\n", + stimulus_strs[stimulus], state_properties[props->state].state_name); + return props->state; + } +} + +static int double_checking_enter(struct attended_transfer_properties *props) +{ + return 0; +} + +static enum attended_transfer_state double_checking_exit(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus) +{ + switch (stimulus) { + case STIMULUS_TRANSFEREE_HANGUP: + play_sound(props->transferer, props->failsound); + publish_transfer_fail(props); + return TRANSFER_FAIL; + case STIMULUS_TRANSFERER_HANGUP: + case STIMULUS_DTMF_ATXFER_COMPLETE: + /* We know the transferer is in the transferee, so take the other bridge off hold */ + bridge_unhold(props->target_bridge); + return TRANSFER_COMPLETE; + case STIMULUS_TRANSFER_TARGET_HANGUP: + case STIMULUS_DTMF_ATXFER_ABORT: + play_sound(props->transferer, props->failsound); + return TRANSFER_RESUME; + case STIMULUS_DTMF_ATXFER_THREEWAY: + bridge_unhold(props->target_bridge); + return TRANSFER_THREEWAY; + case STIMULUS_DTMF_ATXFER_SWAP: + hold(props->transferer); + bridge_move(props->target_bridge, props->transferee_bridge, props->transferer, NULL); + unhold(props->transferer); + return TRANSFER_CONSULTING; + case STIMULUS_NONE: + case STIMULUS_TIMEOUT: + case STIMULUS_TRANSFER_TARGET_ANSWER: + case STIMULUS_RECALL_TARGET_HANGUP: + case STIMULUS_RECALL_TARGET_ANSWER: + default: + ast_log(LOG_WARNING, "Unexpected stimulus '%s' received in attended transfer state '%s'\n", + stimulus_strs[stimulus], state_properties[props->state].state_name); + return props->state; + } +} + +static int complete_enter(struct attended_transfer_properties *props) +{ + bridge_merge(props->transferee_bridge, props->target_bridge, &props->transferer, 1); + play_sound(props->transfer_target, props->xfersound); + publish_transfer_success(props); + return 0; +} + +static int blond_enter(struct attended_transfer_properties *props) +{ + bridge_merge(props->transferee_bridge, props->target_bridge, &props->transferer, 1); + ringing(props->transfer_target); + publish_transfer_success(props); + return 0; +} + +static int blond_nonfinal_enter(struct attended_transfer_properties *props) +{ + int res; + props->superstate = SUPERSTATE_RECALL; + props->recall_target = ast_channel_ref(props->transfer_target); + res = blond_enter(props); + props->transfer_target = ast_channel_unref(props->transfer_target); + return res; +} + +static enum attended_transfer_state blond_nonfinal_exit(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus) +{ + switch (stimulus) { + case STIMULUS_TRANSFEREE_HANGUP: + return TRANSFER_FAIL; + case STIMULUS_RECALL_TARGET_ANSWER: + return TRANSFER_RESUME; + case STIMULUS_TIMEOUT: + ast_softhangup(props->recall_target, AST_SOFTHANGUP_EXPLICIT); + props->recall_target = ast_channel_unref(props->recall_target); + case STIMULUS_RECALL_TARGET_HANGUP: + return TRANSFER_RECALLING; + case STIMULUS_NONE: + case STIMULUS_DTMF_ATXFER_ABORT: + case STIMULUS_DTMF_ATXFER_COMPLETE: + case STIMULUS_DTMF_ATXFER_THREEWAY: + case STIMULUS_DTMF_ATXFER_SWAP: + case STIMULUS_TRANSFERER_HANGUP: + case STIMULUS_TRANSFER_TARGET_HANGUP: + case STIMULUS_TRANSFER_TARGET_ANSWER: + default: + ast_log(LOG_WARNING, "Unexpected stimulus '%s' received in attended transfer state '%s'\n", + stimulus_strs[stimulus], state_properties[props->state].state_name); + return props->state; + } +} + +/*! + * \brief Dial callback when attempting to recall the original transferer channel + * + * This is how we can monitor if the recall target has answered or has hung up. + * If one of the two is detected, then an appropriate stimulus is sent to the + * attended transfer monitor thread. + */ +static void recall_callback(struct ast_dial *dial) +{ + struct attended_transfer_properties *props = ast_dial_get_user_data(dial); + + switch (ast_dial_state(dial)) { + default: + case AST_DIAL_RESULT_INVALID: + case AST_DIAL_RESULT_FAILED: + case AST_DIAL_RESULT_TIMEOUT: + case AST_DIAL_RESULT_HANGUP: + case AST_DIAL_RESULT_UNANSWERED: + /* Failure cases */ + stimulate_attended_transfer(props, STIMULUS_RECALL_TARGET_HANGUP); + break; + case AST_DIAL_RESULT_RINGING: + case AST_DIAL_RESULT_PROGRESS: + case AST_DIAL_RESULT_PROCEEDING: + case AST_DIAL_RESULT_TRYING: + /* Don't care about these cases */ + break; + case AST_DIAL_RESULT_ANSWERED: + /* We struck gold! */ + props->recall_target = ast_dial_answered_steal(dial); + stimulate_attended_transfer(props, STIMULUS_RECALL_TARGET_ANSWER); + break; + } +} + + +static int recalling_enter(struct attended_transfer_properties *props) +{ + RAII_VAR(struct ast_format_cap *, cap, ast_format_cap_alloc_nolock(), ast_format_cap_destroy); + struct ast_format fmt; + + if (!cap) { + return -1; + } + + ast_format_cap_add(cap, ast_format_set(&fmt, AST_FORMAT_SLINEAR, 0)); + + /* When we dial the transfer target, since we are communicating + * with a local channel, we can place the local channel in a bridge + * and then call out to it. When recalling the transferer, though, we + * have to use the dialing API because the channel is not local. + */ + props->dial = ast_dial_create(); + if (!props->dial) { + return -1; + } + + if (ast_dial_append(props->dial, props->transferer_type, props->transferer_addr)) { + return -1; + } + + if (ast_dial_prerun(props->dial, NULL, cap)) { + return -1; + } + + ast_dial_set_state_callback(props->dial, &recall_callback); + + ao2_ref(props, +1); + ast_dial_set_user_data(props->dial, props); + + if (ast_dial_run(props->dial, NULL, 1) == AST_DIAL_RESULT_FAILED) { + ao2_ref(props, -1); + return -1; + } + + bridge_ringing(props->transferee_bridge); + return 0; +} + +static enum attended_transfer_state recalling_exit(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus) +{ + /* No matter what the outcome was, we need to kill off the dial */ + ast_dial_join(props->dial); + ast_dial_destroy(props->dial); + props->dial = NULL; + /* This reference is the one we incremented for the dial state callback (recall_callback) to use */ + ao2_ref(props, -1); + + switch (stimulus) { + case STIMULUS_TRANSFEREE_HANGUP: + return TRANSFER_FAIL; + case STIMULUS_TIMEOUT: + case STIMULUS_RECALL_TARGET_HANGUP: + ++props->retry_attempts; + if (props->retry_attempts >= props->atxfercallbackretries) { + return TRANSFER_FAIL; + } + if (props->atxferloopdelay) { + return TRANSFER_WAIT_TO_RETRANSFER; + } + return TRANSFER_RETRANSFER; + case STIMULUS_RECALL_TARGET_ANSWER: + /* Setting this datastore up will allow the transferer to have all of his + * call features set up automatically when the bridge changes back to a + * normal personality + */ + ast_bridge_features_ds_set(props->recall_target, &props->transferer_features); + ast_channel_ref(props->recall_target); + if (ast_bridge_impart(props->transferee_bridge, props->recall_target, NULL, NULL, 1)) { + ast_hangup(props->recall_target); + return TRANSFER_FAIL; + } + return TRANSFER_RESUME; + case STIMULUS_NONE: + case STIMULUS_DTMF_ATXFER_ABORT: + case STIMULUS_DTMF_ATXFER_COMPLETE: + case STIMULUS_DTMF_ATXFER_THREEWAY: + case STIMULUS_DTMF_ATXFER_SWAP: + case STIMULUS_TRANSFER_TARGET_HANGUP: + case STIMULUS_TRANSFER_TARGET_ANSWER: + case STIMULUS_TRANSFERER_HANGUP: + default: + ast_log(LOG_WARNING, "Unexpected stimulus '%s' received in attended transfer state '%s'\n", + stimulus_strs[stimulus], state_properties[props->state].state_name); + return props->state; + } +} + +static int wait_to_retransfer_enter(struct attended_transfer_properties *props) +{ + bridge_hold(props->transferee_bridge); + return 0; +} + +static enum attended_transfer_state wait_to_retransfer_exit(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus) +{ + bridge_unhold(props->transferee_bridge); + switch (stimulus) { + case STIMULUS_TRANSFEREE_HANGUP: + return TRANSFER_FAIL; + case STIMULUS_TIMEOUT: + return TRANSFER_RETRANSFER; + case STIMULUS_NONE: + case STIMULUS_DTMF_ATXFER_ABORT: + case STIMULUS_DTMF_ATXFER_COMPLETE: + case STIMULUS_DTMF_ATXFER_THREEWAY: + case STIMULUS_DTMF_ATXFER_SWAP: + case STIMULUS_TRANSFER_TARGET_HANGUP: + case STIMULUS_TRANSFER_TARGET_ANSWER: + case STIMULUS_TRANSFERER_HANGUP: + case STIMULUS_RECALL_TARGET_HANGUP: + case STIMULUS_RECALL_TARGET_ANSWER: + default: + ast_log(LOG_WARNING, "Unexpected stimulus '%s' received in attended transfer state '%s'\n", + stimulus_strs[stimulus], state_properties[props->state].state_name); + return props->state; + } +} + +static int attach_framehook(struct attended_transfer_properties *props, struct ast_channel *channel); + +static int retransfer_enter(struct attended_transfer_properties *props) +{ + RAII_VAR(struct ast_format_cap *, cap, ast_format_cap_alloc_nolock(), ast_format_cap_destroy); + struct ast_format fmt; + char destination[AST_MAX_EXTENSION + AST_MAX_CONTEXT + 2]; + int cause; + + if (!cap) { + return -1; + } + + snprintf(destination, sizeof(destination), "%s@%s", props->exten, props->context); + + ast_format_cap_add(cap, ast_format_set(&fmt, AST_FORMAT_SLINEAR, 0)); + + /* Get a channel that is the destination we wish to call */ + props->recall_target = ast_request("Local", cap, NULL, destination, &cause); + if (!props->recall_target) { + ast_log(LOG_ERROR, "Unable to request outbound channel for recall target\n"); + return -1; + } + + if (attach_framehook(props, props->recall_target)) { + ast_log(LOG_ERROR, "Unable to attach framehook to recall target\n"); + ast_hangup(props->recall_target); + props->recall_target = NULL; + return -1; + } + + if (ast_call(props->recall_target, destination, 0)) { + ast_log(LOG_ERROR, "Unable to place outbound call to recall target\n"); + ast_hangup(props->recall_target); + props->recall_target = NULL; + return -1; + } + + ast_channel_ref(props->recall_target); + if (ast_bridge_impart(props->transferee_bridge, props->recall_target, NULL, NULL, 1)) { + ast_log(LOG_ERROR, "Unable to place recall target into bridge\n"); + ast_hangup(props->recall_target); + return -1; + } + + return 0; +} + +static enum attended_transfer_state retransfer_exit(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus) +{ + switch (stimulus) { + case STIMULUS_TRANSFEREE_HANGUP: + return TRANSFER_FAIL; + case STIMULUS_TIMEOUT: + ast_softhangup(props->recall_target, AST_SOFTHANGUP_EXPLICIT); + case STIMULUS_RECALL_TARGET_HANGUP: + props->recall_target = ast_channel_unref(props->recall_target); + if (props->atxferloopdelay) { + return TRANSFER_WAIT_TO_RECALL; + } + return TRANSFER_RECALLING; + case STIMULUS_RECALL_TARGET_ANSWER: + return TRANSFER_RESUME; + case STIMULUS_NONE: + case STIMULUS_DTMF_ATXFER_ABORT: + case STIMULUS_DTMF_ATXFER_COMPLETE: + case STIMULUS_DTMF_ATXFER_THREEWAY: + case STIMULUS_DTMF_ATXFER_SWAP: + case STIMULUS_TRANSFER_TARGET_HANGUP: + case STIMULUS_TRANSFER_TARGET_ANSWER: + case STIMULUS_TRANSFERER_HANGUP: + default: + ast_log(LOG_WARNING, "Unexpected stimulus '%s' received in attended transfer state '%s'\n", + stimulus_strs[stimulus], state_properties[props->state].state_name); + return props->state; + } +} + +static int wait_to_recall_enter(struct attended_transfer_properties *props) +{ + bridge_hold(props->transferee_bridge); + return 0; +} + +static enum attended_transfer_state wait_to_recall_exit(struct attended_transfer_properties *props, + enum attended_transfer_stimulus stimulus) +{ + bridge_unhold(props->transferee_bridge); + switch (stimulus) { + case STIMULUS_TRANSFEREE_HANGUP: + return TRANSFER_FAIL; + case STIMULUS_TIMEOUT: + return TRANSFER_RECALLING; + case STIMULUS_NONE: + case STIMULUS_DTMF_ATXFER_ABORT: + case STIMULUS_DTMF_ATXFER_COMPLETE: + case STIMULUS_DTMF_ATXFER_THREEWAY: + case STIMULUS_DTMF_ATXFER_SWAP: + case STIMULUS_TRANSFER_TARGET_HANGUP: + case STIMULUS_TRANSFER_TARGET_ANSWER: + case STIMULUS_TRANSFERER_HANGUP: + case STIMULUS_RECALL_TARGET_HANGUP: + case STIMULUS_RECALL_TARGET_ANSWER: + default: + ast_log(LOG_WARNING, "Unexpected stimulus '%s' received in attended transfer state '%s'\n", + stimulus_strs[stimulus], state_properties[props->state].state_name); + return props->state; + } +} + +static int fail_enter(struct attended_transfer_properties *props) +{ + if (props->transferee_bridge) { + ast_bridge_destroy(props->transferee_bridge); + props->transferee_bridge = NULL; + } + return 0; +} + +/*! + * \brief DTMF hook when transferer presses abort sequence. + * + * Sends a stimulus to the attended transfer monitor thread that the abort sequence has been pressed + */ +static int atxfer_abort(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel, void *hook_pvt) +{ + struct attended_transfer_properties *props = hook_pvt; + + ast_debug(1, "Transferer on attended transfer %p pressed abort sequence\n", props); + stimulate_attended_transfer(props, STIMULUS_DTMF_ATXFER_ABORT); + return 0; +} + +/*! + * \brief DTMF hook when transferer presses complete sequence. + * + * Sends a stimulus to the attended transfer monitor thread that the complete sequence has been pressed + */ +static int atxfer_complete(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel, void *hook_pvt) +{ + struct attended_transfer_properties *props = hook_pvt; + + ast_debug(1, "Transferer on attended transfer %p pressed complete sequence\n", props); + stimulate_attended_transfer(props, STIMULUS_DTMF_ATXFER_COMPLETE); + return 0; +} + +/*! + * \brief DTMF hook when transferer presses threeway sequence. + * + * Sends a stimulus to the attended transfer monitor thread that the threeway sequence has been pressed + */ +static int atxfer_threeway(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel, void *hook_pvt) +{ + struct attended_transfer_properties *props = hook_pvt; + + ast_debug(1, "Transferer on attended transfer %p pressed threeway sequence\n", props); + stimulate_attended_transfer(props, STIMULUS_DTMF_ATXFER_THREEWAY); + return 0; +} + +/*! + * \brief DTMF hook when transferer presses swap sequence. + * + * Sends a stimulus to the attended transfer monitor thread that the swap sequence has been pressed + */ +static int atxfer_swap(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel, void *hook_pvt) +{ + struct attended_transfer_properties *props = hook_pvt; + + ast_debug(1, "Transferer on attended transfer %p pressed swap sequence\n", props); + stimulate_attended_transfer(props, STIMULUS_DTMF_ATXFER_SWAP); + return 0; +} + +/*! + * \brief Hangup hook for transferer channel. + * + * Sends a stimulus to the attended transfer monitor thread that the transferer has hung up. + */ +static int atxfer_transferer_hangup(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel, void *hook_pvt) +{ + struct attended_transfer_properties *props = hook_pvt; + + ast_debug(1, "Transferer on attended transfer %p hung up\n", props); + stimulate_attended_transfer(props, STIMULUS_TRANSFERER_HANGUP); + return 0; +} + +/*! + * \brief Frame hook for transfer target channel + * + * This is used to determine if the transfer target or recall target has answered + * the outgoing call. + * + * When an answer is detected, a stimulus is sent to the attended transfer monitor + * thread to indicate that the transfer target or recall target has answered. + * + * \param chan The channel the framehook is attached to. + * \param frame The frame being read or written. + * \param event What is being done with the frame. + * \param data The attended transfer properties. + */ +static struct ast_frame *transfer_target_framehook_cb(struct ast_channel *chan, + struct ast_frame *frame, enum ast_framehook_event event, void *data) +{ + struct attended_transfer_properties *props = data; + + if (event == AST_FRAMEHOOK_EVENT_READ && + frame && frame->frametype == AST_FRAME_CONTROL && + frame->subclass.integer == AST_CONTROL_ANSWER) { + + ast_debug(1, "Detected an answer for recall attempt on attended transfer %p\n", props); + if (props->superstate == SUPERSTATE_TRANSFER) { + stimulate_attended_transfer(props, STIMULUS_TRANSFER_TARGET_ANSWER); + } else { + stimulate_attended_transfer(props, STIMULUS_RECALL_TARGET_ANSWER); + } + ast_framehook_detach(chan, props->target_framehook_id); + props->target_framehook_id = -1; + } + + return frame; +} + +static void transfer_target_framehook_destroy_cb(void *data) +{ + struct attended_transfer_properties *props = data; + ao2_cleanup(props); +} + +static int bridge_personality_atxfer_push(struct ast_bridge *self, struct ast_bridge_channel *bridge_channel, struct ast_bridge_channel *swap) +{ + const char *abort_dtmf; + const char *complete_dtmf; + const char *threeway_dtmf; + const char *swap_dtmf; + struct bridge_basic_personality *personality = self->personality; + + if (!ast_channel_has_role(bridge_channel->chan, TRANSFERER_ROLE_NAME)) { + return 0; + } + + abort_dtmf = ast_channel_get_role_option(bridge_channel->chan, TRANSFERER_ROLE_NAME, "abort"); + complete_dtmf = ast_channel_get_role_option(bridge_channel->chan, TRANSFERER_ROLE_NAME, "complete"); + threeway_dtmf = ast_channel_get_role_option(bridge_channel->chan, TRANSFERER_ROLE_NAME, "threeway"); + swap_dtmf = ast_channel_get_role_option(bridge_channel->chan, TRANSFERER_ROLE_NAME, "swap"); + + if (!ast_strlen_zero(abort_dtmf) && ast_bridge_dtmf_hook(bridge_channel->features, + abort_dtmf, atxfer_abort, personality->details[personality->current].pvt, NULL, + AST_BRIDGE_HOOK_REMOVE_ON_PERSONALITY_CHANGE | AST_BRIDGE_HOOK_REMOVE_ON_PULL)) { + return -1; + } + if (!ast_strlen_zero(complete_dtmf) && ast_bridge_dtmf_hook(bridge_channel->features, + complete_dtmf, atxfer_complete, personality->details[personality->current].pvt, NULL, + AST_BRIDGE_HOOK_REMOVE_ON_PERSONALITY_CHANGE | AST_BRIDGE_HOOK_REMOVE_ON_PULL)) { + return -1; + } + if (!ast_strlen_zero(threeway_dtmf) && ast_bridge_dtmf_hook(bridge_channel->features, + threeway_dtmf, atxfer_threeway, personality->details[personality->current].pvt, NULL, + AST_BRIDGE_HOOK_REMOVE_ON_PERSONALITY_CHANGE | AST_BRIDGE_HOOK_REMOVE_ON_PULL)) { + return -1; + } + if (!ast_strlen_zero(swap_dtmf) && ast_bridge_dtmf_hook(bridge_channel->features, + swap_dtmf, atxfer_swap, personality->details[personality->current].pvt, NULL, + AST_BRIDGE_HOOK_REMOVE_ON_PERSONALITY_CHANGE | AST_BRIDGE_HOOK_REMOVE_ON_PULL)) { + return -1; + } + if (ast_bridge_hangup_hook(bridge_channel->features, atxfer_transferer_hangup, + personality->details[personality->current].pvt, NULL, + AST_BRIDGE_HOOK_REMOVE_ON_PERSONALITY_CHANGE | AST_BRIDGE_HOOK_REMOVE_ON_PULL)) { + return -1; + } + + return 0; +} + +static void transfer_pull(struct ast_bridge *self, struct ast_bridge_channel *bridge_channel, struct attended_transfer_properties *props) +{ + if (self->num_channels > 1 || bridge_channel->state == AST_BRIDGE_CHANNEL_STATE_WAIT) { + return; + } + + if (self->num_channels == 1) { + RAII_VAR(struct ast_bridge_channel *, transferer_bridge_channel, NULL, ao2_cleanup); + + ast_channel_lock(props->transferer); + transferer_bridge_channel = ast_channel_get_bridge_channel(props->transferer); + ast_channel_unlock(props->transferer); + + if (!transferer_bridge_channel) { + return; + } + + if (AST_LIST_FIRST(&self->channels) != transferer_bridge_channel) { + return; + } + } + + /* Reaching this point means that either + * 1) The bridge has no channels in it + * 2) The bridge has one channel, and it's the transferer + * In either case, it indicates that the non-transferer parties + * are no longer in the bridge. + */ + if (self == props->transferee_bridge) { + stimulate_attended_transfer(props, STIMULUS_TRANSFEREE_HANGUP); + } else { + stimulate_attended_transfer(props, STIMULUS_TRANSFER_TARGET_HANGUP); + } +} + +static void recall_pull(struct ast_bridge *self, struct ast_bridge_channel *bridge_channel, struct attended_transfer_properties *props) +{ + if (self == props->target_bridge) { + /* Once we're in the recall superstate, we no longer care about this bridge */ + return; + } + + if (bridge_channel->chan == props->recall_target) { + stimulate_attended_transfer(props, STIMULUS_RECALL_TARGET_HANGUP); + return; + } + + if (self->num_channels == 0) { + /* Empty bridge means all transferees are gone for sure */ + stimulate_attended_transfer(props, STIMULUS_TRANSFEREE_HANGUP); + return; + } + + if (self->num_channels == 1) { + RAII_VAR(struct ast_bridge_channel *, target_bridge_channel, NULL, ao2_cleanup); + if (!props->recall_target) { + /* No recall target means that the pull happened on a transferee. If there's still + * a channel left in the bridge, we don't need to send a stimulus + */ + return; + } + + ast_channel_lock(props->recall_target); + target_bridge_channel = ast_channel_get_bridge_channel(props->recall_target); + ast_channel_unlock(props->recall_target); + + if (!target_bridge_channel) { + return; + } + + if (AST_LIST_FIRST(&self->channels) == target_bridge_channel) { + stimulate_attended_transfer(props, STIMULUS_TRANSFEREE_HANGUP); + } + } +} + +static void bridge_personality_atxfer_pull(struct ast_bridge *self, struct ast_bridge_channel *bridge_channel) +{ + struct bridge_basic_personality *personality = self->personality; + struct attended_transfer_properties *props = personality->details[personality->current].pvt; + + switch (props->superstate) { + case SUPERSTATE_TRANSFER: + transfer_pull(self, bridge_channel, props); + break; + case SUPERSTATE_RECALL: + recall_pull(self, bridge_channel, props); + break; + } +} + +static enum attended_transfer_stimulus wait_for_stimulus(struct attended_transfer_properties *props) +{ + RAII_VAR(struct stimulus_list *, list, NULL, ast_free_ptr); + SCOPED_MUTEX(lock, ao2_object_get_lockaddr(props)); + + while (!(list = AST_LIST_REMOVE_HEAD(&props->stimulus_queue, next))) { + if (!(state_properties[props->state].flags & TRANSFER_STATE_FLAG_TIMED)) { + ast_cond_wait(&props->cond, lock); + } else { + struct timeval relative_timeout; + struct timeval absolute_timeout; + struct timespec timeout_arg; + + if (state_properties[props->state].flags & TRANSFER_STATE_FLAG_TIMER_RESET) { + props->start = ast_tvnow(); + } + + if (state_properties[props->state].flags & TRANSFER_STATE_FLAG_TIMER_LOOP_DELAY) { + relative_timeout = ast_samp2tv(props->atxferloopdelay, 1000); + } else { + /* Implied TRANSFER_STATE_FLAG_TIMER_ATXFER_NO_ANSWER */ + relative_timeout = ast_samp2tv(props->atxfernoanswertimeout, 1000); + } + + absolute_timeout = ast_tvadd(props->start, relative_timeout); + timeout_arg.tv_sec = absolute_timeout.tv_sec; + timeout_arg.tv_nsec = absolute_timeout.tv_usec * 1000; + + if (ast_cond_timedwait(&props->cond, lock, &timeout_arg) == ETIMEDOUT) { + return STIMULUS_TIMEOUT; + } + } + } + return list->stimulus; +} + +/*! + * \brief The main loop for the attended transfer monitor thread. + * + * This loop runs continuously until the attended transfer reaches + * a terminal state. Stimuli for changes in the attended transfer + * state are handled in this thread so that all factors in an + * attended transfer can be handled in an orderly fashion. + * + * \param data The attended transfer properties + */ +static void *attended_transfer_monitor_thread(void *data) +{ + struct attended_transfer_properties *props = data; + + for (;;) { + enum attended_transfer_stimulus stimulus; + + ast_debug(1, "About to enter state %s for attended transfer %p\n", state_properties[props->state].state_name, props); + + if (state_properties[props->state].enter && + state_properties[props->state].enter(props)) { + ast_log(LOG_ERROR, "State %s enter function returned an error for attended transfer %p\n", + state_properties[props->state].state_name, props); + break; + } + + if (state_properties[props->state].flags & TRANSFER_STATE_FLAG_TERMINAL) { + ast_debug(1, "State %s is a terminal state. Ending attended transfer %p\n", + state_properties[props->state].state_name, props); + break; + } + + stimulus = wait_for_stimulus(props); + + ast_debug(1, "Received stimulus %s on attended transfer %p\n", stimulus_strs[stimulus], props); + + ast_assert(state_properties[props->state].exit != NULL); + + props->state = state_properties[props->state].exit(props, stimulus); + + ast_debug(1, "Told to enter state %s exit on attended transfer %p\n", state_properties[props->state].state_name, props); + } + + attended_transfer_properties_shutdown(props); + + return NULL; +} + +static int attach_framehook(struct attended_transfer_properties *props, struct ast_channel *channel) +{ + struct ast_framehook_interface target_interface = { + .version = AST_FRAMEHOOK_INTERFACE_VERSION, + .event_cb = transfer_target_framehook_cb, + .destroy_cb = transfer_target_framehook_destroy_cb, + }; + + ao2_ref(props, +1); + target_interface.data = props; + + props->target_framehook_id = ast_framehook_attach(channel, &target_interface); + if (props->target_framehook_id == -1) { + ao2_ref(props, -1); + return -1; + } + return 0; +} + +static int add_transferer_role(struct ast_channel *chan, struct ast_bridge_features_attended_transfer *attended_transfer) +{ + const char *atxfer_abort; + const char *atxfer_threeway; + const char *atxfer_complete; + const char *atxfer_swap; + RAII_VAR(struct ast_features_xfer_config *, xfer_cfg, NULL, ao2_cleanup); + SCOPED_CHANNELLOCK(lock, chan); + + xfer_cfg = ast_get_chan_features_xfer_config(chan); + if (!xfer_cfg) { + return -1; + } + if (attended_transfer) { + atxfer_abort = ast_strdupa(S_OR(attended_transfer->abort, xfer_cfg->atxferabort)); + atxfer_threeway = ast_strdupa(S_OR(attended_transfer->threeway, xfer_cfg->atxferthreeway)); + atxfer_complete = ast_strdupa(S_OR(attended_transfer->complete, xfer_cfg->atxfercomplete)); + atxfer_swap = ast_strdupa(S_OR(attended_transfer->swap, xfer_cfg->atxferswap)); + } else { + atxfer_abort = ast_strdupa(xfer_cfg->atxferabort); + atxfer_threeway = ast_strdupa(xfer_cfg->atxferthreeway); + atxfer_complete = ast_strdupa(xfer_cfg->atxfercomplete); + atxfer_swap = ast_strdupa(xfer_cfg->atxferswap); + } + + return ast_channel_add_bridge_role(chan, TRANSFERER_ROLE_NAME) || + ast_channel_set_bridge_role_option(chan, TRANSFERER_ROLE_NAME, "abort", atxfer_abort) || + ast_channel_set_bridge_role_option(chan, TRANSFERER_ROLE_NAME, "complete", atxfer_complete) || + ast_channel_set_bridge_role_option(chan, TRANSFERER_ROLE_NAME, "threeway", atxfer_threeway) || + ast_channel_set_bridge_role_option(chan, TRANSFERER_ROLE_NAME, "swap", atxfer_swap); +} + +/*! + * \brief Helper function that presents dialtone and grabs extension + * + * \retval 0 on success + * \retval -1 on failure + */ +static int grab_transfer(struct ast_channel *chan, char *exten, size_t exten_len, const char *context) +{ + int res; + int digit_timeout; + RAII_VAR(struct ast_features_xfer_config *, xfer_cfg, NULL, ao2_cleanup); + + ast_channel_lock(chan); + xfer_cfg = ast_get_chan_features_xfer_config(chan); + if (!xfer_cfg) { + ast_log(LOG_ERROR, "Unable to get transfer configuration\n"); + ast_channel_unlock(chan); + return -1; + } + digit_timeout = xfer_cfg->transferdigittimeout; + ast_channel_unlock(chan); + + /* Play the simple "transfer" prompt out and wait */ + res = ast_stream_and_wait(chan, "pbx-transfer", AST_DIGIT_ANY); + ast_stopstream(chan); + if (res < 0) { + /* Hangup or error */ + return -1; + } + if (res) { + /* Store the DTMF digit that interrupted playback of the file. */ + exten[0] = res; + } + + /* Drop to dialtone so they can enter the extension they want to transfer to */ + res = ast_app_dtget(chan, context, exten, exten_len, exten_len - 1, digit_timeout); + if (res < 0) { + /* Hangup or error */ + res = -1; + } else if (!res) { + /* 0 for invalid extension dialed. */ + if (ast_strlen_zero(exten)) { + ast_debug(1, "%s dialed no digits.\n", ast_channel_name(chan)); + } else { + ast_debug(1, "%s dialed '%s@%s' does not exist.\n", + ast_channel_name(chan), exten, context); + } + ast_stream_and_wait(chan, "pbx-invalid", AST_DIGIT_NONE); + res = -1; + } else { + /* Dialed extension is valid. */ + res = 0; + } + return res; +} + +static void copy_caller_data(struct ast_channel *dest, struct ast_channel *caller) +{ + ast_channel_lock_both(caller, dest); + ast_connected_line_copy_from_caller(ast_channel_connected(dest), ast_channel_caller(caller)); + ast_channel_inherit_variables(caller, dest); + ast_channel_datastore_inherit(caller, dest); + ast_channel_unlock(dest); + ast_channel_unlock(caller); +} + +/*! \brief Helper function that creates an outgoing channel and returns it immediately */ +static struct ast_channel *dial_transfer(struct ast_channel *caller, const char *destination) +{ + struct ast_channel *chan; + int cause; + + /* Now we request a local channel to prepare to call the destination */ + chan = ast_request("Local", ast_channel_nativeformats(caller), caller, destination, + &cause); + if (!chan) { + return NULL; + } + + /* Who is transferring the call. */ + pbx_builtin_setvar_helper(chan, "TRANSFERERNAME", ast_channel_name(caller)); + + /* To work as an analog to BLINDTRANSFER */ + pbx_builtin_setvar_helper(chan, "ATTENDEDTRANSFER", ast_channel_name(caller)); + + /* Before we actually dial out let's inherit appropriate information. */ + copy_caller_data(chan, caller); + + return chan; +} + +/*! + * \brief Internal built in feature for attended transfers + * + * This hook will set up a thread for monitoring the progress of + * an attended transfer. For more information about attended transfer + * progress, see documentation on the transfer state machine. + * + * \param bridge Bridge where attended transfer DTMF sequence was entered + * \param bridge_channel The channel that pressed the attended transfer DTMF sequence + * \param hook_pvt Structure with further information about the attended transfer + */ +static int feature_attended_transfer(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel, void *hook_pvt) +{ + struct ast_bridge_features_attended_transfer *attended_transfer = hook_pvt; + struct attended_transfer_properties *props; + char destination[AST_MAX_EXTENSION + AST_MAX_CONTEXT + 1]; + char exten[AST_MAX_EXTENSION] = ""; + pthread_t thread; + + if (strcmp(bridge->v_table->name, "basic")) { + ast_log(LOG_ERROR, "Attended transfer attempted on unsupported bridge type '%s'\n", bridge->v_table->name); + return 0; + } + + if (bridge->inhibit_merge) { + ast_log(LOG_ERROR, "Unable to perform attended transfer since bridge '%s' does not permit merging.\n", bridge->uniqueid); + return 0; + } + + props = attended_transfer_properties_alloc(bridge, bridge_channel->chan, + attended_transfer ? attended_transfer->context : NULL); + + if (!props) { + ast_log(LOG_ERROR, "Unable to allocate control structure for performing attended transfer\n"); + return 0; + } + + if (add_transferer_role(props->transferer, attended_transfer)) { + ast_log(LOG_ERROR, "Unable to set transferer bridge role properly\n"); + attended_transfer_properties_shutdown(props); + return 0; + } + + ast_bridge_channel_write_hold(bridge_channel, NULL); + props->transferee_bridge = ast_bridge_channel_merge_inhibit(bridge_channel, +1); + + /* Grab the extension to transfer to */ + if (grab_transfer(bridge_channel->chan, exten, sizeof(exten), props->context)) { + ast_log(LOG_WARNING, "Unable to acquire target extension for attended transfer\n"); + ast_bridge_channel_write_unhold(bridge_channel); + attended_transfer_properties_shutdown(props); + return 0; + } + + ast_string_field_set(props, exten, exten); + + /* Fill the variable with the extension and context we want to call */ + snprintf(destination, sizeof(destination), "%s@%s", props->exten, props->context); + + ast_debug(1, "Attended transfer to '%s'\n", destination); + + /* Get a channel that is the destination we wish to call */ + props->transfer_target = dial_transfer(bridge_channel->chan, destination); + if (!props->transfer_target) { + ast_log(LOG_ERROR, "Unable to request outbound channel for attended transfer target\n"); + ast_stream_and_wait(props->transferer, props->failsound, AST_DIGIT_NONE); + ast_bridge_channel_write_unhold(bridge_channel); + attended_transfer_properties_shutdown(props); + return 0; + } + + + /* Create a bridge to use to talk to the person we are calling */ + props->target_bridge = ast_bridge_basic_new(); + if (!props->target_bridge) { + ast_log(LOG_ERROR, "Unable to create bridge for attended transfer target\n"); + ast_stream_and_wait(props->transferer, props->failsound, AST_DIGIT_NONE); + ast_bridge_channel_write_unhold(bridge_channel); + ast_hangup(props->transfer_target); + props->transfer_target = NULL; + attended_transfer_properties_shutdown(props); + return 0; + } + ast_bridge_merge_inhibit(props->target_bridge, +1); + + if (attach_framehook(props, props->transfer_target)) { + ast_log(LOG_ERROR, "Unable to attach framehook to transfer target\n"); + ast_stream_and_wait(props->transferer, props->failsound, AST_DIGIT_NONE); + ast_bridge_channel_write_unhold(bridge_channel); + ast_hangup(props->transfer_target); + props->transfer_target = NULL; + attended_transfer_properties_shutdown(props); + return 0; + } + + bridge_basic_change_personality(props->target_bridge, + BRIDGE_BASIC_PERSONALITY_ATXFER, props); + bridge_basic_change_personality(bridge, + BRIDGE_BASIC_PERSONALITY_ATXFER, props); + + if (ast_call(props->transfer_target, destination, 0)) { + ast_log(LOG_ERROR, "Unable to place outbound call to transfer target\n"); + ast_stream_and_wait(bridge_channel->chan, props->failsound, AST_DIGIT_NONE); + ast_bridge_channel_write_unhold(bridge_channel); + ast_hangup(props->transfer_target); + props->transfer_target = NULL; + attended_transfer_properties_shutdown(props); + return 0; + } + + /* We increase the refcount of the transfer target because ast_bridge_impart() will + * steal the reference we already have. We need to keep a reference, so the only + * choice is to give it a bump + */ + ast_channel_ref(props->transfer_target); + if (ast_bridge_impart(props->target_bridge, props->transfer_target, NULL, NULL, 1)) { + ast_log(LOG_ERROR, "Unable to place transfer target into bridge\n"); + ast_stream_and_wait(bridge_channel->chan, props->failsound, AST_DIGIT_NONE); + ast_bridge_channel_write_unhold(bridge_channel); + ast_hangup(props->transfer_target); + attended_transfer_properties_shutdown(props); + return 0; + } + + if (ast_pthread_create_detached(&thread, NULL, attended_transfer_monitor_thread, props)) { + ast_log(LOG_ERROR, "Unable to create monitoring thread for attended transfer\n"); + ast_stream_and_wait(bridge_channel->chan, props->failsound, AST_DIGIT_NONE); + ast_bridge_channel_write_unhold(bridge_channel); + attended_transfer_properties_shutdown(props); + return 0; + } + + /* Once the monitoring thread has been created, it is responsible for destroying all + * of the necessary components. + */ + return 0; +} + +static void blind_transfer_cb(struct ast_channel *new_channel, void *user_data, + enum ast_transfer_type transfer_type) +{ + struct ast_channel *transferer_channel = user_data; + + if (transfer_type == AST_BRIDGE_TRANSFER_MULTI_PARTY) { + copy_caller_data(new_channel, transferer_channel); + } +} + +/*! \brief Internal built in feature for blind transfers */ +static int feature_blind_transfer(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel, void *hook_pvt) +{ + char exten[AST_MAX_EXTENSION] = ""; + struct ast_bridge_features_blind_transfer *blind_transfer = hook_pvt; + const char *context; + char *goto_on_blindxfr; + + if (strcmp(bridge->v_table->name, "basic")) { + ast_log(LOG_ERROR, "Blind transfer attempted on unsupported bridge type '%s'\n", bridge->v_table->name); + return 0; + } + + ast_bridge_channel_write_hold(bridge_channel, NULL); + + ast_channel_lock(bridge_channel->chan); + context = ast_strdupa(get_transfer_context(bridge_channel->chan, + blind_transfer ? blind_transfer->context : NULL)); + goto_on_blindxfr = ast_strdupa(S_OR(pbx_builtin_getvar_helper(bridge_channel->chan, + "GOTO_ON_BLINDXFR"), "")); + ast_channel_unlock(bridge_channel->chan); + + /* Grab the extension to transfer to */ + if (grab_transfer(bridge_channel->chan, exten, sizeof(exten), context)) { + ast_bridge_channel_write_unhold(bridge_channel); + return 0; + } + + if (!ast_strlen_zero(goto_on_blindxfr)) { + ast_debug(1, "After transfer, transferer %s goes to %s\n", + ast_channel_name(bridge_channel->chan), goto_on_blindxfr); + ast_after_bridge_set_go_on(bridge_channel->chan, NULL, NULL, 0, goto_on_blindxfr); + } + + if (ast_bridge_transfer_blind(0, bridge_channel->chan, exten, context, blind_transfer_cb, + bridge_channel->chan) != AST_BRIDGE_TRANSFER_SUCCESS && + !ast_strlen_zero(goto_on_blindxfr)) { + ast_after_bridge_goto_discard(bridge_channel->chan); + } + + return 0; +} + struct ast_bridge_methods ast_bridge_basic_v_table; +struct ast_bridge_methods personality_normal_v_table; +struct ast_bridge_methods personality_atxfer_v_table; + +static void bridge_basic_change_personality(struct ast_bridge *bridge, + enum bridge_basic_personality_type type, void *user_data) +{ + struct bridge_basic_personality *personality = bridge->personality; + SCOPED_LOCK(lock, bridge, ast_bridge_lock, ast_bridge_unlock); + + remove_hooks_on_personality_change(bridge); + + ao2_cleanup(personality->details[personality->current].pvt); + personality->details[personality->current].pvt = NULL; + ast_clear_flag(&bridge->feature_flags, AST_FLAGS_ALL); + + personality->current = type; + if (user_data) { + ao2_ref(user_data, +1); + } + personality->details[personality->current].pvt = user_data; + ast_set_flag(&bridge->feature_flags, personality->details[personality->current].bridge_flags); + if (personality->details[personality->current].on_personality_change) { + personality->details[personality->current].on_personality_change(bridge); + } +} + +static void personality_destructor(void *obj) +{ + struct bridge_basic_personality *personality = obj; + int i; + + for (i = 0; i < BRIDGE_BASIC_PERSONALITY_END; ++i) { + ao2_cleanup(personality->details[i].pvt); + } +} + +static void on_personality_change_normal(struct ast_bridge *bridge) +{ + struct ast_bridge_channel *iter; + + AST_LIST_TRAVERSE(&bridge->channels, iter, entry) { + if (add_normal_hooks(bridge, iter)) { + ast_log(LOG_WARNING, "Unable to set up bridge hooks for channel %s. Features may not work properly\n", + ast_channel_name(iter->chan)); + } + } +} + +static void init_details(struct personality_details *details, + enum bridge_basic_personality_type type) +{ + switch (type) { + case BRIDGE_BASIC_PERSONALITY_NORMAL: + details->v_table = &personality_normal_v_table; + details->bridge_flags = NORMAL_FLAGS; + details->on_personality_change = on_personality_change_normal; + break; + case BRIDGE_BASIC_PERSONALITY_ATXFER: + details->v_table = &personality_atxfer_v_table; + details->bridge_flags = TRANSFER_FLAGS; + break; + default: + ast_log(LOG_WARNING, "Asked to initialize unexpected basic bridge personality type.\n"); + break; + } +} + +static struct ast_bridge *bridge_basic_personality_alloc(struct ast_bridge *bridge) +{ + struct bridge_basic_personality *personality; + int i; + + if (!bridge) { + return NULL; + } + + personality = ao2_alloc(sizeof(*personality), personality_destructor); + if (!personality) { + ao2_ref(bridge, -1); + return NULL; + } + for (i = 0; i < BRIDGE_BASIC_PERSONALITY_END; ++i) { + init_details(&personality->details[i], i); + } + personality->current = BRIDGE_BASIC_PERSONALITY_NORMAL; + bridge->personality = personality; + + return bridge; +} struct ast_bridge *ast_bridge_basic_new(void) { - void *bridge; + struct ast_bridge *bridge; bridge = ast_bridge_alloc(sizeof(struct ast_bridge), &ast_bridge_basic_v_table); bridge = ast_bridge_base_init(bridge, AST_BRIDGE_CAPABILITY_NATIVE | AST_BRIDGE_CAPABILITY_1TO1MIX - | AST_BRIDGE_CAPABILITY_MULTIMIX, - AST_BRIDGE_FLAG_DISSOLVE_HANGUP | AST_BRIDGE_FLAG_DISSOLVE_EMPTY - | AST_BRIDGE_FLAG_SMART); + | AST_BRIDGE_CAPABILITY_MULTIMIX, NORMAL_FLAGS); + bridge = bridge_basic_personality_alloc(bridge); bridge = ast_bridge_register(bridge); return bridge; } @@ -165,4 +2778,19 @@ void ast_bridging_init_basic(void) ast_bridge_basic_v_table = ast_bridge_base_v_table; ast_bridge_basic_v_table.name = "basic"; ast_bridge_basic_v_table.push = bridge_basic_push; + ast_bridge_basic_v_table.pull = bridge_basic_pull; + ast_bridge_basic_v_table.destroy = bridge_basic_destroy; + + personality_normal_v_table = ast_bridge_base_v_table; + personality_normal_v_table.name = "normal"; + personality_normal_v_table.push = bridge_personality_normal_push; + + personality_atxfer_v_table = ast_bridge_base_v_table; + personality_atxfer_v_table.name = "attended transfer"; + personality_atxfer_v_table.push = bridge_personality_atxfer_push; + personality_atxfer_v_table.pull = bridge_personality_atxfer_pull; + + ast_bridge_features_register(AST_BRIDGE_BUILTIN_ATTENDEDTRANSFER, feature_attended_transfer, NULL); + ast_bridge_features_register(AST_BRIDGE_BUILTIN_BLINDTRANSFER, feature_blind_transfer, NULL); } + diff --git a/main/bridging_roles.c b/main/bridging_roles.c index a50ade406..c20b0ccf1 100644 --- a/main/bridging_roles.c +++ b/main/bridging_roles.c @@ -374,6 +374,26 @@ int ast_channel_set_bridge_role_option(struct ast_channel *channel, const char * return setup_bridge_role_option(role, option, value); } +int ast_channel_has_role(struct ast_channel *channel, const char *role_name) +{ + return get_role_from_channel(channel, role_name) ? 1 : 0; +} + +const char *ast_channel_get_role_option(struct ast_channel *channel, const char *role_name, const char *option) +{ + struct bridge_role *role; + struct bridge_role_option *role_option; + + role = get_role_from_channel(channel, role_name); + if (!role) { + return NULL; + } + + role_option = get_role_option(role, option); + + return role_option ? role_option->value : NULL; +} + int ast_bridge_channel_has_role(struct ast_bridge_channel *bridge_channel, const char *role_name) { if (!bridge_channel->bridge_roles) { diff --git a/main/cel.c b/main/cel.c index 3d0e8e991..9d21cc8f5 100644 --- a/main/cel.c +++ b/main/cel.c @@ -1433,9 +1433,10 @@ static void cel_attended_transfer_cb( switch (xfer->dest_type) { case AST_ATTENDED_TRANSFER_DEST_FAIL: return; - /* handle these two the same */ + /* handle these three the same */ case AST_ATTENDED_TRANSFER_DEST_BRIDGE_MERGE: case AST_ATTENDED_TRANSFER_DEST_LINK: + case AST_ATTENDED_TRANSFER_DEST_THREEWAY: extra = ast_json_pack("{s: s, s: s, s: s}", "bridge1_id", bridge1->uniqueid, "channel2_name", channel2->name, diff --git a/main/features.c b/main/features.c index 928a81e94..1150f7eb1 100644 --- a/main/features.c +++ b/main/features.c @@ -3212,37 +3212,37 @@ static int setup_bridge_features_builtin(struct ast_bridge_features *features, s if (!builtin_feature_get_exten(chan, "blindxfer", dtmf, sizeof(dtmf)) && !ast_strlen_zero(dtmf)) { res |= ast_bridge_features_enable(features, AST_BRIDGE_BUILTIN_BLINDTRANSFER, dtmf, - NULL, NULL, AST_BRIDGE_HOOK_REMOVE_ON_PULL); + NULL, NULL, AST_BRIDGE_HOOK_REMOVE_ON_PULL | AST_BRIDGE_HOOK_REMOVE_ON_PERSONALITY_CHANGE); } if (!builtin_feature_get_exten(chan, "atxfer", dtmf, sizeof(dtmf)) && !ast_strlen_zero(dtmf)) { res |= ast_bridge_features_enable(features, AST_BRIDGE_BUILTIN_ATTENDEDTRANSFER, dtmf, - NULL, NULL, AST_BRIDGE_HOOK_REMOVE_ON_PULL); + NULL, NULL, AST_BRIDGE_HOOK_REMOVE_ON_PULL | AST_BRIDGE_HOOK_REMOVE_ON_PERSONALITY_CHANGE); } } if (ast_test_flag(flags, AST_FEATURE_DISCONNECT) && !builtin_feature_get_exten(chan, "disconnect", dtmf, sizeof(dtmf)) && !ast_strlen_zero(dtmf)) { res |= ast_bridge_features_enable(features, AST_BRIDGE_BUILTIN_HANGUP, dtmf, - NULL, NULL, AST_BRIDGE_HOOK_REMOVE_ON_PULL); + NULL, NULL, AST_BRIDGE_HOOK_REMOVE_ON_PULL | AST_BRIDGE_HOOK_REMOVE_ON_PERSONALITY_CHANGE); } if (ast_test_flag(flags, AST_FEATURE_PARKCALL) && !builtin_feature_get_exten(chan, "parkcall", dtmf, sizeof(dtmf)) && !ast_strlen_zero(dtmf)) { res |= ast_bridge_features_enable(features, AST_BRIDGE_BUILTIN_PARKCALL, dtmf, - NULL, NULL, AST_BRIDGE_HOOK_REMOVE_ON_PULL); + NULL, NULL, AST_BRIDGE_HOOK_REMOVE_ON_PULL | AST_BRIDGE_HOOK_REMOVE_ON_PERSONALITY_CHANGE); } if (ast_test_flag(flags, AST_FEATURE_AUTOMON) && !builtin_feature_get_exten(chan, "automon", dtmf, sizeof(dtmf)) && !ast_strlen_zero(dtmf)) { res |= ast_bridge_features_enable(features, AST_BRIDGE_BUILTIN_AUTOMON, dtmf, - NULL, NULL, AST_BRIDGE_HOOK_REMOVE_ON_PULL); + NULL, NULL, AST_BRIDGE_HOOK_REMOVE_ON_PULL | AST_BRIDGE_HOOK_REMOVE_ON_PERSONALITY_CHANGE); } if (ast_test_flag(flags, AST_FEATURE_AUTOMIXMON) && !builtin_feature_get_exten(chan, "automixmon", dtmf, sizeof(dtmf)) && !ast_strlen_zero(dtmf)) { res |= ast_bridge_features_enable(features, AST_BRIDGE_BUILTIN_AUTOMIXMON, dtmf, - NULL, NULL, AST_BRIDGE_HOOK_REMOVE_ON_PULL); + NULL, NULL, AST_BRIDGE_HOOK_REMOVE_ON_PULL | AST_BRIDGE_HOOK_REMOVE_ON_PERSONALITY_CHANGE); } return res ? -1 : 0; diff --git a/main/features_config.c b/main/features_config.c index 66fdbfae8..bebd593d1 100644 --- a/main/features_config.c +++ b/main/features_config.c @@ -106,6 +106,16 @@ the transferees, and the transfer target all being in a single bridge together.</para> </description> </configOption> + <configOption name="atxferswap" default="*4"> + <synopsis>Digits to dial to toggle who the transferrer is currently bridged to during an attended transfer</synopsis> + <description> + <para>This option is only available to the transferrer during an attended + transfer operation. Pressing this DTMF sequence will result in the transferrer swapping + which party he is bridged with. For instance, if the transferrer is currently bridged with + the transfer target, then pressing this DTMF sequence will cause the transferrer to be + bridged with the transferees.</para> + </description> + </configOption> <configOption name="pickupexten" default="*8"> <synopsis>Digits used for picking up ringing calls</synopsis> <description> @@ -355,6 +365,7 @@ #define DEFAULT_ATXFER_ABORT "*1" #define DEFAULT_ATXFER_COMPLETE "*2" #define DEFAULT_ATXFER_THREEWAY "*3" +#define DEFAULT_ATXFER_SWAP "*4" /*! Default pickup options */ #define DEFAULT_PICKUPEXTEN "*8" @@ -846,6 +857,8 @@ static int xfer_set(struct ast_features_xfer_config *xfer, const char *name, ast_string_field_set(xfer, atxfercomplete, value); } else if (!strcasecmp(name, "atxferthreeway")) { ast_string_field_set(xfer, atxferthreeway, value); + } else if (!strcasecmp(name, "atxferswap")) { + ast_string_field_set(xfer, atxferswap, value); } else { /* Unrecognized option */ res = -1; @@ -879,6 +892,8 @@ static int xfer_get(struct ast_features_xfer_config *xfer, const char *field, ast_copy_string(buf, xfer->atxfercomplete, len); } else if (!strcasecmp(field, "atxferthreeway")) { ast_copy_string(buf, xfer->atxferthreeway, len); + } else if (!strcasecmp(field, "atxferswap")) { + ast_copy_string(buf, xfer->atxferswap, len); } else { /* Unrecognized option */ res = -1; @@ -1629,6 +1644,8 @@ static int load_config(void) DEFAULT_ATXFER_COMPLETE, xfer_handler, 0); aco_option_register_custom(&cfg_info, "atxferthreeway", ACO_EXACT, global_options, DEFAULT_ATXFER_THREEWAY, xfer_handler, 0); + aco_option_register_custom(&cfg_info, "atxferswap", ACO_EXACT, global_options, + DEFAULT_ATXFER_SWAP, xfer_handler, 0); aco_option_register_custom(&cfg_info, "pickupexten", ACO_EXACT, global_options, DEFAULT_PICKUPEXTEN, pickup_handler, 0); diff --git a/main/stasis_bridging.c b/main/stasis_bridging.c index dcf4275ae..43ec006a1 100644 --- a/main/stasis_bridging.c +++ b/main/stasis_bridging.c @@ -221,12 +221,13 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") <enum name="Bridge"><para>The transfer was accomplished by merging two bridges into one.</para></enum> <enum name="App"><para>The transfer was accomplished by having a channel or bridge run a dialplan application.</para></enum> <enum name="Link"><para>The transfer was accomplished by linking two bridges together using a local channel pair.</para></enum> + <enum name="Threeway"><para>The transfer was accomplished by placing all parties into a threeway call.</para></enum> <enum name="Fail"><para>The transfer failed.</para></enum> </enumlist> </parameter> <parameter name="DestBridgeUniqueid"> <para>Indicates the surviving bridge when bridges were merged to complete the transfer</para> - <note><para>This header is only present when <replaceable>DestType</replaceable> is <literal>Bridge</literal></para></note> + <note><para>This header is only present when <replaceable>DestType</replaceable> is <literal>Bridge</literal> or <literal>Threeway</literal></para></note> </parameter> <parameter name="DestApp"> <para>Indicates the application that is running when the transfer completes</para> @@ -333,6 +334,10 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") <parameter name="LocalTwoUniqueid"> <note><para>This header is only present when <replaceable>DestType</replaceable> is <literal>Link</literal></para></note> </parameter> + <parameter name="DestTransfererChannel"> + <para>The name of the surviving transferer channel when a transfer results in a threeway call</para> + <note><para>This header is only present when <replaceable>DestType</replaceable> is <literal>Threeway</literal></para></note> + </parameter> </syntax> <description> <para>The headers in this event attempt to describe all the major details of the attended transfer. The two transferer channels @@ -835,6 +840,11 @@ static struct ast_manager_event_blob *attended_transfer_to_ami(struct stasis_mes ast_str_append(&variable_data, 0, "%s", ast_str_buffer(local1_state)); ast_str_append(&variable_data, 0, "%s", ast_str_buffer(local2_state)); break; + case AST_ATTENDED_TRANSFER_DEST_THREEWAY: + ast_str_append(&variable_data, 0, "DestType: Threeway\r\n"); + ast_str_append(&variable_data, 0, "DestBridgeUniqueid: %s\r\n", transfer_msg->dest.threeway.bridge_snapshot->uniqueid); + ast_str_append(&variable_data, 0, "DestTransfererChannel: %s\r\n", transfer_msg->dest.threeway.channel_snapshot->name); + break; case AST_ATTENDED_TRANSFER_DEST_FAIL: ast_str_append(&variable_data, 0, "DestType: Fail\r\n"); break; @@ -941,6 +951,39 @@ void ast_bridge_publish_attended_transfer_bridge_merge(int is_external, enum ast stasis_publish(ast_bridge_topic_all(), msg); } +void ast_bridge_publish_attended_transfer_threeway(int is_external, enum ast_transfer_result result, + struct ast_bridge_channel_pair *transferee, struct ast_bridge_channel_pair *target, + struct ast_bridge_channel_pair *final_pair) +{ + RAII_VAR(struct ast_attended_transfer_message *, transfer_msg, NULL, ao2_cleanup); + RAII_VAR(struct stasis_message *, msg, NULL, ao2_cleanup); + + transfer_msg = attended_transfer_message_create(is_external, result, transferee, target); + if (!transfer_msg) { + return; + } + + transfer_msg->dest_type = AST_ATTENDED_TRANSFER_DEST_THREEWAY; + if (final_pair->channel == transferee->channel) { + transfer_msg->dest.threeway.channel_snapshot = transfer_msg->to_transferee.channel_snapshot; + } else { + transfer_msg->dest.threeway.channel_snapshot = transfer_msg->to_transfer_target.channel_snapshot; + } + + if (final_pair->bridge == transferee->bridge) { + transfer_msg->dest.threeway.bridge_snapshot = transfer_msg->to_transferee.bridge_snapshot; + } else { + transfer_msg->dest.threeway.bridge_snapshot = transfer_msg->to_transfer_target.bridge_snapshot; + } + + msg = stasis_message_create(ast_attended_transfer_type(), transfer_msg); + if (!msg) { + return; + } + + stasis_publish(ast_bridge_topic_all(), msg); +} + void ast_bridge_publish_attended_transfer_app(int is_external, enum ast_transfer_result result, struct ast_bridge_channel_pair *transferee, struct ast_bridge_channel_pair *target, const char *dest_app) |