diff --git a/CHANGES b/CHANGES index d347507435d03c78b4aacf79fa7692d8d8182f2e..60d8a4dea1fd33da97d042dda638a9f319800d2a 100644 --- a/CHANGES +++ b/CHANGES @@ -83,6 +83,20 @@ ARI types defined in the "disallowed" list are not sent to the application. Note that if a type is specified in both lists "disallowed" takes precedence. + * A new REST API call has been added: 'move'. It follows the format + 'channels/{channelId}/move' and can be used to move channels from one application + to another without needing to exit back into the dialplan. An application must be + specified, but the passing a list of arguments to the new application is optional. + An example call would look like this: + + client.channels.move(channelId=chan.id, app='ari-example', appArgs='a,b,c') + + If the channel was inside of a bridge when switching applications, it will + remain there. If the application specified cannot be moved to, then the channel + will remain in the current application and an event will be triggered named + "ApplicationMoveFailed", which will provide the destination application's name + and the channel information. + res_pjsip ------------------ * A new configuration parameter "taskprocessor_overload_trigger" has been diff --git a/include/asterisk/stasis_app.h b/include/asterisk/stasis_app.h index c40e901e5f39dcc7694b9c138d179e5182c7b60a..01c7ff4d7cbd9bff31504cc794de9dbd9473dff6 100644 --- a/include/asterisk/stasis_app.h +++ b/include/asterisk/stasis_app.h @@ -501,6 +501,20 @@ void stasis_app_control_clear_roles(struct stasis_app_control *control); */ int stasis_app_control_continue(struct stasis_app_control *control, const char *context, const char *extension, int priority); +/*! + * \brief Exit \c res_stasis and move to another Stasis application. + * + * If the channel is no longer in \c res_stasis, this function does nothing. + * + * \param control Control for \c res_stasis + * \param app_name The name of the application to switch to + * \param app_args The list of arguments to pass to the application + * + * \return 0 for success + * \return -1 for error + */ +int stasis_app_control_move(struct stasis_app_control *control, const char *app_name, const char *app_args); + /*! * \brief Redirect a channel in \c res_stasis to a particular endpoint * diff --git a/res/ari/ari_model_validators.c b/res/ari/ari_model_validators.c index 44d9d7727cc7daf4ff0d0c0c37a82adc4474999e..254de3c271bf028cc42ba5c5401cd806b0fdb05f 100644 --- a/res/ari/ari_model_validators.c +++ b/res/ari/ari_model_validators.c @@ -2028,6 +2028,127 @@ ari_validator ast_ari_validate_mailbox_fn(void) return ast_ari_validate_mailbox; } +int ast_ari_validate_application_move_failed(struct ast_json *json) +{ + int res = 1; + struct ast_json_iter *iter; + int has_type = 0; + int has_application = 0; + int has_args = 0; + int has_channel = 0; + int has_destination = 0; + + for (iter = ast_json_object_iter(json); iter; iter = ast_json_object_iter_next(json, iter)) { + if (strcmp("asterisk_id", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + prop_is_valid = ast_ari_validate_string( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field asterisk_id failed validation\n"); + res = 0; + } + } else + if (strcmp("type", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + has_type = 1; + prop_is_valid = ast_ari_validate_string( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field type failed validation\n"); + res = 0; + } + } else + if (strcmp("application", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + has_application = 1; + prop_is_valid = ast_ari_validate_string( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field application failed validation\n"); + res = 0; + } + } else + if (strcmp("timestamp", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + prop_is_valid = ast_ari_validate_date( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field timestamp failed validation\n"); + res = 0; + } + } else + if (strcmp("args", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + has_args = 1; + prop_is_valid = ast_ari_validate_list( + ast_json_object_iter_value(iter), + ast_ari_validate_string); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field args failed validation\n"); + res = 0; + } + } else + if (strcmp("channel", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + has_channel = 1; + prop_is_valid = ast_ari_validate_channel( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field channel failed validation\n"); + res = 0; + } + } else + if (strcmp("destination", ast_json_object_iter_key(iter)) == 0) { + int prop_is_valid; + has_destination = 1; + prop_is_valid = ast_ari_validate_string( + ast_json_object_iter_value(iter)); + if (!prop_is_valid) { + ast_log(LOG_ERROR, "ARI ApplicationMoveFailed field destination failed validation\n"); + res = 0; + } + } else + { + ast_log(LOG_ERROR, + "ARI ApplicationMoveFailed has undocumented field %s\n", + ast_json_object_iter_key(iter)); + res = 0; + } + } + + if (!has_type) { + ast_log(LOG_ERROR, "ARI ApplicationMoveFailed missing required field type\n"); + res = 0; + } + + if (!has_application) { + ast_log(LOG_ERROR, "ARI ApplicationMoveFailed missing required field application\n"); + res = 0; + } + + if (!has_args) { + ast_log(LOG_ERROR, "ARI ApplicationMoveFailed missing required field args\n"); + res = 0; + } + + if (!has_channel) { + ast_log(LOG_ERROR, "ARI ApplicationMoveFailed missing required field channel\n"); + res = 0; + } + + if (!has_destination) { + ast_log(LOG_ERROR, "ARI ApplicationMoveFailed missing required field destination\n"); + res = 0; + } + + return res; +} + +ari_validator ast_ari_validate_application_move_failed_fn(void) +{ + return ast_ari_validate_application_move_failed; +} + int ast_ari_validate_application_replaced(struct ast_json *json) { int res = 1; @@ -5095,6 +5216,9 @@ int ast_ari_validate_event(struct ast_json *json) if (strcmp("Event", discriminator) == 0) { /* Self type; fall through */ } else + if (strcmp("ApplicationMoveFailed", discriminator) == 0) { + return ast_ari_validate_application_move_failed(json); + } else if (strcmp("ApplicationReplaced", discriminator) == 0) { return ast_ari_validate_application_replaced(json); } else @@ -5293,6 +5417,9 @@ int ast_ari_validate_message(struct ast_json *json) if (strcmp("Message", discriminator) == 0) { /* Self type; fall through */ } else + if (strcmp("ApplicationMoveFailed", discriminator) == 0) { + return ast_ari_validate_application_move_failed(json); + } else if (strcmp("ApplicationReplaced", discriminator) == 0) { return ast_ari_validate_application_replaced(json); } else diff --git a/res/ari/ari_model_validators.h b/res/ari/ari_model_validators.h index 1ee74f4865b83eaaf54b1a74b333dfb40f2040f5..4b3269af6a73d3fd9f3492e45c3e7dde97562f2f 100644 --- a/res/ari/ari_model_validators.h +++ b/res/ari/ari_model_validators.h @@ -623,6 +623,24 @@ int ast_ari_validate_mailbox(struct ast_json *json); */ ari_validator ast_ari_validate_mailbox_fn(void); +/*! + * \brief Validator for ApplicationMoveFailed. + * + * Notification that trying to move a channel to another Stasis application failed. + * + * \param json JSON object to validate. + * \returns True (non-zero) if valid. + * \returns False (zero) if invalid. + */ +int ast_ari_validate_application_move_failed(struct ast_json *json); + +/*! + * \brief Function pointer to ast_ari_validate_application_move_failed(). + * + * See \ref ast_ari_model_validators.h for more details. + */ +ari_validator ast_ari_validate_application_move_failed_fn(void); + /*! * \brief Validator for ApplicationReplaced. * @@ -1527,6 +1545,14 @@ ari_validator ast_ari_validate_application_fn(void); * - name: string (required) * - new_messages: int (required) * - old_messages: int (required) + * ApplicationMoveFailed + * - asterisk_id: string + * - type: string (required) + * - application: string (required) + * - timestamp: Date + * - args: List[string] (required) + * - channel: Channel (required) + * - destination: string (required) * ApplicationReplaced * - asterisk_id: string * - type: string (required) diff --git a/res/ari/resource_channels.c b/res/ari/resource_channels.c index 08f97f1d34b68e3f5810bc555f2a3c5c144c092c..eca70cec8a7a972a7da44b0fe6f7262c805dc938 100644 --- a/res/ari/resource_channels.c +++ b/res/ari/resource_channels.c @@ -217,6 +217,26 @@ void ast_ari_channels_continue_in_dialplan( ast_ari_response_no_content(response); } +void ast_ari_channels_move(struct ast_variable *headers, + struct ast_ari_channels_move_args *args, + struct ast_ari_response *response) +{ + RAII_VAR(struct stasis_app_control *, control, NULL, ao2_cleanup); + + control = find_control(response, args->channel_id); + if (!control) { + return; + } + + if (stasis_app_control_move(control, args->app, args->app_args)) { + ast_ari_response_error(response, 500, "Internal Server Error", + "Failed to switch Stasis applications"); + return; + } + + ast_ari_response_no_content(response); +} + void ast_ari_channels_redirect(struct ast_variable *headers, struct ast_ari_channels_redirect_args *args, struct ast_ari_response *response) diff --git a/res/ari/resource_channels.h b/res/ari/resource_channels.h index b071d08baf237a59bcbbc7b2994e16783770cdeb..fdd7a6b7812d775bab15afa202a180567a35561a 100644 --- a/res/ari/resource_channels.h +++ b/res/ari/resource_channels.h @@ -261,6 +261,34 @@ int ast_ari_channels_continue_in_dialplan_parse_body( * \param[out] response HTTP response */ void ast_ari_channels_continue_in_dialplan(struct ast_variable *headers, struct ast_ari_channels_continue_in_dialplan_args *args, struct ast_ari_response *response); +/*! Argument struct for ast_ari_channels_move() */ +struct ast_ari_channels_move_args { + /*! Channel's id */ + const char *channel_id; + /*! The channel will be passed to this Stasis application. */ + const char *app; + /*! The application arguments to pass to the Stasis application provided by 'app'. */ + const char *app_args; +}; +/*! + * \brief Body parsing function for /channels/{channelId}/move. + * \param body The JSON body from which to parse parameters. + * \param[out] args The args structure to parse into. + * \retval zero on success + * \retval non-zero on failure + */ +int ast_ari_channels_move_parse_body( + struct ast_json *body, + struct ast_ari_channels_move_args *args); + +/*! + * \brief Move the channel from one Stasis application to another. + * + * \param headers HTTP headers + * \param args Swagger parameters + * \param[out] response HTTP response + */ +void ast_ari_channels_move(struct ast_variable *headers, struct ast_ari_channels_move_args *args, struct ast_ari_response *response); /*! Argument struct for ast_ari_channels_redirect() */ struct ast_ari_channels_redirect_args { /*! Channel's id */ diff --git a/res/res_ari_channels.c b/res/res_ari_channels.c index dae146c43c0a93a1345d04f635960596fb6ea62e..3d96d60e4cd8d74f17fc3460222bd1f763f7481b 100644 --- a/res/res_ari_channels.c +++ b/res/res_ari_channels.c @@ -775,6 +775,95 @@ static void ast_ari_channels_continue_in_dialplan_cb( } #endif /* AST_DEVMODE */ +fin: __attribute__((unused)) + return; +} +int ast_ari_channels_move_parse_body( + struct ast_json *body, + struct ast_ari_channels_move_args *args) +{ + struct ast_json *field; + /* Parse query parameters out of it */ + field = ast_json_object_get(body, "app"); + if (field) { + args->app = ast_json_string_get(field); + } + field = ast_json_object_get(body, "appArgs"); + if (field) { + args->app_args = ast_json_string_get(field); + } + return 0; +} + +/*! + * \brief Parameter parsing callback for /channels/{channelId}/move. + * \param get_params GET parameters in the HTTP request. + * \param path_vars Path variables extracted from the request. + * \param headers HTTP headers. + * \param[out] response Response to the HTTP request. + */ +static void ast_ari_channels_move_cb( + struct ast_tcptls_session_instance *ser, + struct ast_variable *get_params, struct ast_variable *path_vars, + struct ast_variable *headers, struct ast_json *body, struct ast_ari_response *response) +{ + struct ast_ari_channels_move_args args = {}; + struct ast_variable *i; +#if defined(AST_DEVMODE) + int is_valid; + int code; +#endif /* AST_DEVMODE */ + + for (i = get_params; i; i = i->next) { + if (strcmp(i->name, "app") == 0) { + args.app = (i->value); + } else + if (strcmp(i->name, "appArgs") == 0) { + args.app_args = (i->value); + } else + {} + } + for (i = path_vars; i; i = i->next) { + if (strcmp(i->name, "channelId") == 0) { + args.channel_id = (i->value); + } else + {} + } + if (ast_ari_channels_move_parse_body(body, &args)) { + ast_ari_response_alloc_failed(response); + goto fin; + } + ast_ari_channels_move(headers, &args, response); +#if defined(AST_DEVMODE) + code = response->response_code; + + switch (code) { + case 0: /* Implementation is still a stub, or the code wasn't set */ + is_valid = response->message == NULL; + break; + case 500: /* Internal Server Error */ + case 501: /* Not Implemented */ + case 404: /* Channel not found */ + case 409: /* Channel not in a Stasis application */ + is_valid = 1; + break; + default: + if (200 <= code && code <= 299) { + is_valid = ast_ari_validate_void( + response->message); + } else { + ast_log(LOG_ERROR, "Invalid error response %d for /channels/{channelId}/move\n", code); + is_valid = 0; + } + } + + if (!is_valid) { + ast_log(LOG_ERROR, "Response validation failed for /channels/{channelId}/move\n"); + ast_ari_response_error(response, 500, + "Internal Server Error", "Response validation failed"); + } +#endif /* AST_DEVMODE */ + fin: __attribute__((unused)) return; } @@ -2680,6 +2769,15 @@ static struct stasis_rest_handlers channels_channelId_continue = { .children = { } }; /*! \brief REST handler for /api-docs/channels.json */ +static struct stasis_rest_handlers channels_channelId_move = { + .path_segment = "move", + .callbacks = { + [AST_HTTP_POST] = ast_ari_channels_move_cb, + }, + .num_children = 0, + .children = { } +}; +/*! \brief REST handler for /api-docs/channels.json */ static struct stasis_rest_handlers channels_channelId_redirect = { .path_segment = "redirect", .callbacks = { @@ -2831,8 +2929,8 @@ static struct stasis_rest_handlers channels_channelId = { [AST_HTTP_POST] = ast_ari_channels_originate_with_id_cb, [AST_HTTP_DELETE] = ast_ari_channels_hangup_cb, }, - .num_children = 14, - .children = { &channels_channelId_continue,&channels_channelId_redirect,&channels_channelId_answer,&channels_channelId_ring,&channels_channelId_dtmf,&channels_channelId_mute,&channels_channelId_hold,&channels_channelId_moh,&channels_channelId_silence,&channels_channelId_play,&channels_channelId_record,&channels_channelId_variable,&channels_channelId_snoop,&channels_channelId_dial, } + .num_children = 15, + .children = { &channels_channelId_continue,&channels_channelId_move,&channels_channelId_redirect,&channels_channelId_answer,&channels_channelId_ring,&channels_channelId_dtmf,&channels_channelId_mute,&channels_channelId_hold,&channels_channelId_moh,&channels_channelId_silence,&channels_channelId_play,&channels_channelId_record,&channels_channelId_variable,&channels_channelId_snoop,&channels_channelId_dial, } }; /*! \brief REST handler for /api-docs/channels.json */ static struct stasis_rest_handlers channels = { diff --git a/res/res_stasis.c b/res/res_stasis.c index 4da78a6dc0fe25c19b6e6d243d28126189c27db4..4b7c3ed74cf2f643c18dc9528adaf68e3ee3e17b 100644 --- a/res/res_stasis.c +++ b/res/res_stasis.c @@ -1324,7 +1324,17 @@ int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc, control = control_create(chan, app); if (!control) { - ast_log(LOG_ERROR, "Allocated failed\n"); + ast_log(LOG_ERROR, "Control allocation failed or Stasis app '%s' not registered\n", app_name); + return -1; + } + + if (!control_app(control)) { + ast_log(LOG_ERROR, "Stasis app '%s' not registered\n", app_name); + return -1; + } + + if (!app_is_active(control_app(control))) { + ast_log(LOG_ERROR, "Stasis app '%s' not active\n", app_name); return -1; } ao2_link(app_controls, control); @@ -1334,7 +1344,7 @@ int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc, return -1; } - res = send_start_msg(app, chan, argc, argv); + res = send_start_msg(control_app(control), chan, argc, argv); if (res != 0) { ast_log(LOG_ERROR, "Error sending start message to '%s'\n", app_name); @@ -1357,15 +1367,138 @@ int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc, break; } + /* control->next_app is only modified within the control thread, so this is safe */ + if (control_next_app(control)) { + struct stasis_app *next_app = ao2_find(apps_registry, control_next_app(control), OBJ_SEARCH_KEY); + + if (next_app && app_is_active(next_app)) { + int idx; + int next_argc; + char **next_argv; + + /* If something goes wrong in this conditional, res will need to be non-zero + * so that the code below the exec loop knows something went wrong during a move. + */ + if (!stasis_app_channel_is_stasis_end_published(chan)) { + res = has_masquerade_store(chan) && app_send_end_msg(control_app(control), chan); + if (res != 0) { + ast_log(LOG_ERROR, + "Error sending end message to %s\n", stasis_app_name(control_app(control))); + control_mark_done(control); + ao2_ref(next_app, -1); + break; + } + } else { + remove_stasis_end_published(chan); + } + + /* This will ao2_bump next_app, and unref the previous app by 1 */ + control_set_app(control, next_app); + + /* There's a chance that the previous application is ready for clean up, so go ahead + * and do that now. + */ + cleanup(); + + /* We need to add another masquerade store, otherwise the leave message will + * not show up for the correct application. + */ + if (add_masquerade_store(chan)) { + ast_log(LOG_ERROR, "Failed to attach masquerade detector\n"); + res = -1; + control_mark_done(control); + ao2_ref(next_app, -1); + break; + } + + /* We MUST get the size before the list, as control_next_app_args steals the elements + * from the string vector. + */ + next_argc = control_next_app_args_size(control); + next_argv = control_next_app_args(control); + + res = send_start_msg(control_app(control), chan, next_argc, next_argv); + + /* Even if res != 0, we still need to free the memory we got from control_argv */ + if (next_argv) { + for (idx = 0; idx < next_argc; idx++) { + ast_free(next_argv[idx]); + } + ast_free(next_argv); + } + + if (res != 0) { + ast_log(LOG_ERROR, + "Error sending start message to '%s'\n", stasis_app_name(control_app(control))); + remove_masquerade_store(chan); + control_mark_done(control); + ao2_ref(next_app, -1); + break; + } + + /* Done switching applications, free memory and clean up */ + control_move_cleanup(control); + } else { + /* If we can't switch applications, do nothing */ + struct ast_json *msg; + RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup); + + if (!next_app) { + ast_log(LOG_ERROR, "Could not move to Stasis app '%s' - not registered\n", + control_next_app(control)); + } else { + ast_log(LOG_ERROR, "Could not move to Stasis app '%s' - not active\n", + control_next_app(control)); + } + + snapshot = ast_channel_snapshot_get_latest(ast_channel_uniqueid(chan)); + if (!snapshot) { + ast_log(LOG_ERROR, "Could not get channel shapshot for '%s'\n", + ast_channel_name(chan)); + } else { + struct ast_json *json_args; + int next_argc = control_next_app_args_size(control); + char **next_argv = control_next_app_args(control); + + msg = ast_json_pack("{s: s, s: o, s: s, s: []}", + "type", "ApplicationMoveFailed", + "channel", ast_channel_snapshot_to_json(snapshot, NULL), + "destination", control_next_app(control), + "args"); + json_args = ast_json_object_get(msg, "args"); + if (!json_args) { + ast_log(LOG_ERROR, "Could not get args json array"); + } else { + int r = 0; + int idx; + for (idx = 0; idx < next_argc; ++idx) { + r = ast_json_array_append(json_args, + ast_json_string_create(next_argv[idx])); + if (r != 0) { + ast_log(LOG_ERROR, "Error appending to ApplicationMoveFailed message\n"); + break; + } + } + if (r == 0) { + app_send(control_app(control), msg); + } + } + ast_json_unref(msg); + } + } + control_move_cleanup(control); + ao2_cleanup(next_app); + } + last_bridge = bridge; bridge = ao2_bump(stasis_app_get_bridge(control)); if (bridge != last_bridge) { if (last_bridge) { - app_unsubscribe_bridge(app, last_bridge); + app_unsubscribe_bridge(control_app(control), last_bridge); } if (bridge) { - app_subscribe_bridge(app, bridge); + app_subscribe_bridge(control_app(control), bridge); } } @@ -1425,18 +1558,18 @@ int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc, } if (stasis_app_get_bridge(control)) { - app_unsubscribe_bridge(app, stasis_app_get_bridge(control)); + app_unsubscribe_bridge(control_app(control), stasis_app_get_bridge(control)); } ao2_cleanup(bridge); /* Only publish a stasis_end event if it hasn't already been published */ - if (!stasis_app_channel_is_stasis_end_published(chan)) { + if (!res && !stasis_app_channel_is_stasis_end_published(chan)) { /* A masquerade has occurred and this message will be wrong so it * has already been sent elsewhere. */ - res = has_masquerade_store(chan) && app_send_end_msg(app, chan); + res = has_masquerade_store(chan) && app_send_end_msg(control_app(control), chan); if (res != 0) { ast_log(LOG_ERROR, - "Error sending end message to %s\n", app_name); + "Error sending end message to %s\n", stasis_app_name(control_app(control))); return res; } } else { @@ -1456,12 +1589,10 @@ int stasis_app_exec(struct ast_channel *chan, const char *app_name, int argc, /* The control needs to be removed from the controls container in * case a new PBX is started and ends up coming back into Stasis. */ - ao2_cleanup(app); - app = NULL; control_unlink(control); control = NULL; - if (!ast_channel_pbx(chan)) { + if (!res && !ast_channel_pbx(chan)) { int chan_hungup; /* The ASYNCGOTO softhangup flag may have broken the channel out of diff --git a/res/stasis/control.c b/res/stasis/control.c index 5b3b048757c052c218e843109b04069fe9d606dd..3e16e803f23182f768925bba7b507cd904a4fb9b 100644 --- a/res/stasis/control.c +++ b/res/stasis/control.c @@ -83,9 +83,19 @@ struct stasis_app_control { */ struct ast_silence_generator *silgen; /*! - * The app for which this control was created + * The app for which this control is currently controlling. + * This can change through the use of the /channels/{channelId}/move + * command. */ struct stasis_app *app; + /*! + * The name of the next Stasis application to move to. + */ + char *next_app; + /*! + * The list of arguments to pass to StasisStart when moving to another app. + */ + AST_VECTOR(, char *) next_app_args; /*! * When set, /c app_stasis should exit and continue in the dialplan. */ @@ -101,6 +111,8 @@ static void control_dtor(void *obj) ast_channel_cleanup(control->channel); ao2_cleanup(control->app); + control_move_cleanup(control); + ast_cond_destroy(&control->wait_cond); AST_LIST_HEAD_DESTROY(&control->add_rules); AST_LIST_HEAD_DESTROY(&control->remove_rules); @@ -141,6 +153,9 @@ struct stasis_app_control *control_create(struct ast_channel *channel, struct st return NULL; } + control->next_app = NULL; + AST_VECTOR_INIT(&control->next_app_args, 0); + return control; } @@ -391,6 +406,73 @@ int stasis_app_control_continue(struct stasis_app_control *control, const char * return 0; } +struct stasis_app_control_move_data { + char *app_name; + char *app_args; +}; + +static int app_control_move(struct stasis_app_control *control, + struct ast_channel *chan, void *data) +{ + struct stasis_app_control_move_data *move_data = data; + + control->next_app = ast_strdup(move_data->app_name); + if (!control->next_app) { + ast_log(LOG_ERROR, "Allocation failed for next app\n"); + return -1; + } + + if (move_data->app_args) { + char *token; + + while ((token = strtok_r(move_data->app_args, ",", &move_data->app_args))) { + int res; + char *arg; + + if (!(arg = ast_strdup(token))) { + ast_log(LOG_ERROR, "Allocation failed for next app arg\n"); + control_move_cleanup(control); + return -1; + } + + res = AST_VECTOR_APPEND(&control->next_app_args, arg); + if (res) { + ast_log(LOG_ERROR, "Failed to append arg to next app args\n"); + ast_free(arg); + control_move_cleanup(control); + return -1; + } + } + } + + return 0; +} + +int stasis_app_control_move(struct stasis_app_control *control, const char *app_name, const char *app_args) +{ + struct stasis_app_control_move_data *move_data; + size_t size; + + size = sizeof(*move_data) + strlen(app_name) + strlen(app_args) + 2; + if (!(move_data = ast_calloc(1, size))) { + return -1; + } + + move_data->app_name = (char *)move_data + sizeof(*move_data); + move_data->app_args = move_data->app_name + strlen(app_name) + 1; + + strcpy(move_data->app_name, app_name); /* Safe */ + if (app_args) { + strcpy(move_data->app_args, app_args); /* Safe */ + } else { + move_data->app_args = NULL; + } + + stasis_app_send_command_async(control, app_control_move, move_data, ast_free_ptr); + + return 0; +} + static int app_control_redirect(struct stasis_app_control *control, struct ast_channel *chan, void *data) { @@ -1575,3 +1657,32 @@ void stasis_app_control_shutdown(void) } ast_mutex_unlock(&dial_bridge_lock); } + +void control_set_app(struct stasis_app_control *control, struct stasis_app *app) +{ + ao2_cleanup(control->app); + control->app = ao2_bump(app); +} + +char *control_next_app(struct stasis_app_control *control) +{ + return control->next_app; +} + +void control_move_cleanup(struct stasis_app_control *control) +{ + ast_free(control->next_app); + control->next_app = NULL; + + AST_VECTOR_RESET(&control->next_app_args, ast_free_ptr); +} + +char **control_next_app_args(struct stasis_app_control *control) +{ + return AST_VECTOR_STEAL_ELEMENTS(&control->next_app_args); +} + +int control_next_app_args_size(struct stasis_app_control *control) +{ + return AST_VECTOR_SIZE(&control->next_app_args); +} diff --git a/res/stasis/control.h b/res/stasis/control.h index 868a8091bb4014bbcf9067f44e4b8929cdb40849..67aa3b70fe456e07c077db458284f22a995324be 100644 --- a/res/stasis/control.h +++ b/res/stasis/control.h @@ -107,6 +107,58 @@ int control_prestart_dispatch_all(struct stasis_app_control *control, */ struct stasis_app *control_app(struct stasis_app_control *control); +/*! + * \brief Set the application the control object belongs to + * + * \param control The control for the channel + * \param app The application this control will now belong to + * + * \note This will unref control's previous app by 1, and bump app by 1 + */ +void control_set_app(struct stasis_app_control *control, struct stasis_app *app); + +/*! + * \brief Returns the name of the application we are moving to + * + * \param control The control for the channel + * + * \return The name of the application we are moving to + */ +char *control_next_app(struct stasis_app_control *control); + +/*! + * \brief Free any memory that was allocated for switching applications via + * /channels/{channelId}/move + * + * \param control The control for the channel + */ +void control_move_cleanup(struct stasis_app_control *control); + +/*! + * \brief Returns the list of arguments to pass to the application we are moving to + * + * \note If you wish to get the size of the list, control_next_app_args_size should be + * called before this, as this function will steal the elements from the string vector + * and set the size to 0. + * + * \param control The control for the channel + * + * \return The arguments to pass to the application we are moving to + */ +char **control_next_app_args(struct stasis_app_control *control); + +/*! + * \brief Returns the number of arguments to be passed to the application we are moving to + * + * \note This should always be called before control_next_app_args, as calling that function + * will steal all elements from the string vector and set the size to 0. + * + * \param control The control for the channel + * + * \return The number of arguments to be passed to the application we are moving to + */ +int control_next_app_args_size(struct stasis_app_control *control); + /*! * \brief Command callback for adding a channel to a bridge * diff --git a/rest-api/api-docs/channels.json b/rest-api/api-docs/channels.json index 08db22467a5615a28abe24f6bcf3f7acc010a510..616193421efba295186241af23bcbfbc2cde9d06 100644 --- a/rest-api/api-docs/channels.json +++ b/rest-api/api-docs/channels.json @@ -515,6 +515,54 @@ } ] }, + { + "path": "/channels/{channelId}/move", + "description": "Move the channel from one Stasis application to another.", + "operations": [ + { + "httpMethod": "POST", + "summary": "Move the channel from one Stasis application to another.", + "nickname": "move", + "responseClass": "void", + "parameters": [ + { + "name": "channelId", + "description": "Channel's id", + "paramType": "path", + "required": true, + "allowMultiple": false, + "dataType": "string" + }, + { + "name": "app", + "description": "The channel will be passed to this Stasis application.", + "paramType": "query", + "required": true, + "allowMultiple": false, + "dataType": "string" + }, + { + "name": "appArgs", + "description": "The application arguments to pass to the Stasis application provided by 'app'.", + "paramType": "query", + "required": false, + "allowMultiple": false, + "dataType": "string" + } + ], + "errorResponses": [ + { + "code": "404", + "reason": "Channel not found" + }, + { + "code": "409", + "reason": "Channel not in a Stasis application" + } + ] + } + ] + }, { "path": "/channels/{channelId}/redirect", "description": "Inform the channel that it should redirect itself to a different location. Note that this will almost certainly cause the channel to exit the application.", diff --git a/rest-api/api-docs/events.json b/rest-api/api-docs/events.json index d85d8d9fe4fabde2b3bcd6cbc36655cb0b2316eb..c9f4b6ae422fb4f2a872f3cb991c074a83e1a283 100644 --- a/rest-api/api-docs/events.json +++ b/rest-api/api-docs/events.json @@ -159,6 +159,7 @@ "RecordingStarted", "RecordingFinished", "RecordingFailed", + "ApplicationMoveFailed", "ApplicationReplaced", "BridgeCreated", "BridgeDestroyed", @@ -335,6 +336,25 @@ } } }, + "ApplicationMoveFailed": { + "id": "ApplicationMoveFailed", + "description": "Notification that trying to move a channel to another Stasis application failed.", + "properties": { + "channel": { + "required": true, + "type": "Channel" + }, + "destination": { + "required": true, + "type": "string" + }, + "args": { + "required": true, + "type": "List[string]", + "description": "Arguments to the application" + } + } + }, "ApplicationReplaced": { "id": "ApplicationReplaced", "description": "Notification that another WebSocket has taken over for an application.\n\nAn application may only be subscribed to by a single WebSocket at a time. If multiple WebSockets attempt to subscribe to the same application, the newer WebSocket wins, and the older one receives this event.",