From 1166dd3252f32361ce31c131c6d63eaa875f2cf6 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Sat, 25 Aug 2012 23:12:55 +0000 Subject: [PATCH 1/4] updating VERSION file --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 64d9f206b82..ae8bc4c96a7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.51 +v2.00.10 From f552fd45c990b20eebed89246a013d50362b8fdd Mon Sep 17 00:00:00 2001 From: karl anderson Date: Mon, 10 Sep 2012 21:30:38 +0000 Subject: [PATCH 2/4] WHISTLE-1609: if the flag is empty ignore it --- whistle_apps/apps/stepswitch/src/stepswitch_util.erl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/whistle_apps/apps/stepswitch/src/stepswitch_util.erl b/whistle_apps/apps/stepswitch/src/stepswitch_util.erl index 30a3a7f0c3e..90f6d5c57bc 100644 --- a/whistle_apps/apps/stepswitch/src/stepswitch_util.erl +++ b/whistle_apps/apps/stepswitch/src/stepswitch_util.erl @@ -59,7 +59,10 @@ evaluate_number(Number, Resrcs) -> evaluate_flags(F1, Resrcs) -> [Resrc || #resrc{flags=F2}=Resrc <- Resrcs, - lists:all(fun(Flag) -> lists:member(Flag, F2) end, F1) + lists:all(fun(Flag) -> + wh_util:is_empty(Flag) + orelse lists:member(Flag, F2) + end, F1) ]. %%-------------------------------------------------------------------- From f396535d1d770177079b215d0e396a4ff4c6fb64 Mon Sep 17 00:00:00 2001 From: Jon Blanton Date: Tue, 30 Oct 2012 01:44:28 +0000 Subject: [PATCH 3/4] UMOJO-42: Attach custom sip headers from FreeSWITCH events to AMQP messages --- ecallmgr/src/ecallmgr_call_events.erl | 2 ++ ecallmgr/src/ecallmgr_util.erl | 12 +++++++++++- lib/whistle-1.0.0/src/api/wapi_call.erl | 3 ++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/ecallmgr/src/ecallmgr_call_events.erl b/ecallmgr/src/ecallmgr_call_events.erl index 40190302816..84eaf03d536 100644 --- a/ecallmgr/src/ecallmgr_call_events.erl +++ b/ecallmgr/src/ecallmgr_call_events.erl @@ -364,6 +364,7 @@ create_event(EventName, ApplicationName, Props) -> -spec create_event_props/3 :: (binary(), 'undefined' | ne_binary(), proplist()) -> proplist(). create_event_props(EventName, ApplicationName, Props) -> CCVs = ecallmgr_util:custom_channel_vars(Props), + CustomSipHeaders = ecallmgr_util:custom_sip_headers(Props), {Mega,Sec,Micro} = erlang:now(), Timestamp = wh_util:to_binary(((Mega * 1000000 + Sec) * 1000000 + Micro)), props:filter_undefined( @@ -392,6 +393,7 @@ create_event_props(EventName, ApplicationName, Props) -> ,{<<"Fax-Bad-Rows">>, props:get_value(<<"variable_fax_bad_rows">>, Props)} ,{<<"Fax-Transfer-Rate">>, props:get_value(<<"variable_fax_transfer_rate">>, Props)} ,{<<"Custom-Channel-Vars">>, wh_json:from_list(CCVs)} + ,{<<"Custom-SIP-Headers">>, wh_json:from_list(CustomSipHeaders)} %% this sucks, its leaky but I dont see a better way around it since we need the raw application %% name in call_control... (see note in call_control on start_link for why we need to use AMQP %% to communicate to it) diff --git a/ecallmgr/src/ecallmgr_util.erl b/ecallmgr/src/ecallmgr_util.erl index d0ba45aa19e..c5b34e76ea4 100644 --- a/ecallmgr/src/ecallmgr_util.erl +++ b/ecallmgr/src/ecallmgr_util.erl @@ -14,7 +14,8 @@ -export([send_cmd/4]). -export([get_expires/1]). -export([get_interface_properties/1, get_interface_properties/2]). --export([get_sip_to/1, get_sip_from/1, get_sip_request/1, get_orig_ip/1, custom_channel_vars/1]). +-export([get_sip_to/1, get_sip_from/1, get_sip_request/1, get_orig_ip/1]). +-export([custom_channel_vars/1, custom_sip_headers/1]). -export([eventstr_to_proplist/1, varstr_to_proplist/1, get_setting/1, get_setting/2]). -export([is_node_up/1, is_node_up/2]). -export([fs_log/3, put_callid/1]). @@ -208,6 +209,15 @@ custom_channel_vars(Prop) -> (_, Acc) -> Acc end, [], Prop). +-spec custom_sip_headers/1 :: (proplist()) -> proplist(). +custom_sip_headers(Prop) -> + lists:foldl(fun({<<"variable_sip_h_", Header/binary>>, V}, Acc) -> [{Header, wh_util:to_binary(mochiweb_util:unquote(V))} | Acc]; + ({<<"variable_sip_rh_", Header/binary>>, V}, Acc) -> [{Header, wh_util:to_binary(mochiweb_util:unquote(V))} | Acc]; + ({<<"variable_sip_ph_", Header/binary>>, V}, Acc) -> [{Header, wh_util:to_binary(mochiweb_util:unquote(V))} | Acc]; + ({<<"variable_sip_bye_h_", Header/binary>>, V}, Acc) -> [{Header, wh_util:to_binary(mochiweb_util:unquote(V))} | Acc]; + (_, Acc) -> Acc + end, [], Prop). + %% convert a raw FS string of headers to a proplist %% "Event-Name: NAME\nEvent-Timestamp: 1234\n" -> [{<<"Event-Name">>, <<"NAME">>}, {<<"Event-Timestamp">>, <<"1234">>}] -spec eventstr_to_proplist/1 :: (ne_binary() | nonempty_string()) -> proplist(). diff --git a/lib/whistle-1.0.0/src/api/wapi_call.erl b/lib/whistle-1.0.0/src/api/wapi_call.erl index 5e8502ff6e1..21cd3748e23 100644 --- a/lib/whistle-1.0.0/src/api/wapi_call.erl +++ b/lib/whistle-1.0.0/src/api/wapi_call.erl @@ -47,7 +47,8 @@ %% Call Events -define(CALL_EVENT_HEADERS, [<<"Call-ID">>]). -define(OPTIONAL_CALL_EVENT_HEADERS, [<<"Application-Name">>, <<"Application-Response">> - ,<<"Custom-Channel-Vars">>, <<"Timestamp">>, <<"Channel-State">> + ,<<"Custom-Channel-Vars">>, <<"Custom-SIP-Headers">> + ,<<"Timestamp">>, <<"Channel-State">> ,<<"Call-Direction">>, <<"Transfer-History">> ,<<"Other-Leg-Direction">>, <<"Other-Leg-Caller-ID-Name">> ,<<"Other-Leg-Caller-ID-Number">>, <<"Other-Leg-Destination-Number">> From 97063870b24789906d30270834bad6525b7cf413 Mon Sep 17 00:00:00 2001 From: Jon Blanton Date: Wed, 31 Oct 2012 01:49:55 +0000 Subject: [PATCH 4/4] UMOJO-42: Added cf_speech_ivr and cb_speech_ivr --- speech_ivr.README | 22 ++ .../callflow/src/module/cf_speech_ivr.erl | 94 +++++++++ .../crossbar/src/modules/cb_speech_ivr.erl | 196 ++++++++++++++++++ 3 files changed, 312 insertions(+) create mode 100644 speech_ivr.README create mode 100644 whistle_apps/apps/callflow/src/module/cf_speech_ivr.erl create mode 100644 whistle_apps/apps/crossbar/src/modules/cb_speech_ivr.erl diff --git a/speech_ivr.README b/speech_ivr.README new file mode 100644 index 00000000000..642f3b8c7fc --- /dev/null +++ b/speech_ivr.README @@ -0,0 +1,22 @@ +==================================== SETUP ==================================== + +1) Ensure that cb_speech_ivr is in the "autoload_modules" list in the crossbar + system_config document. + + crossbar system_config document: + http://host.example.com:5984/_utils/document.html?system_config/crossbar + + +2) Ensure that the speech_ivr system_config document exists. If it does not, + create a new document in the system_config database and seed it with the + following JSON: + { + "_id": "speech_ivr", + "default": { + "sip_uri_host": "someserver.example.com;transport=tcp", + "bridge_timeout": 3 + } + } + + speech_ivr system_config document: + http://host.example.org:5984/_utils/document.html?system_config/speech_ivr diff --git a/whistle_apps/apps/callflow/src/module/cf_speech_ivr.erl b/whistle_apps/apps/callflow/src/module/cf_speech_ivr.erl new file mode 100644 index 00000000000..1e19f2ee677 --- /dev/null +++ b/whistle_apps/apps/callflow/src/module/cf_speech_ivr.erl @@ -0,0 +1,94 @@ +%%%------------------------------------------------------------------- +%%% @copyright (C) 2011-2012, Umojo +%%% @doc +%%% +%%% @end +%%% @contributors +%%% Jon Blanton +%%%------------------------------------------------------------------- +-module(cf_speech_ivr). + +-include("../callflow.hrl"). + +-export([handle/2]). + +-define(CONFIG_CAT, <<"speech_ivr">>). +%%-------------------------------------------------------------------- +%% @public +%% @doc +%% Entry point for this module, attempts to call the speech IVR as defined +%% Returns continue if fails to connect or stop when successfull. +%% @end +%%-------------------------------------------------------------------- +-spec handle/2 :: (wh_json:json_object(), whapps_call:call()) -> 'ok'. +handle(Data, Call) -> + case bridge_to_speech_ivr(Data, Call) of + {ok, HangupData} -> + %% success - branch/transfer to correct child + CustomSipHeaders = wh_json:get_value(<<"Custom-SIP-Headers">>, HangupData), + Instructions = {wh_json:get_ne_value(<<"X-Branch">>, CustomSipHeaders) + ,wh_json:get_ne_value(<<"X-Transfer">>, CustomSipHeaders) + }, + + case Instructions of + {undefined, undefined} -> + lager:debug("no instructions found"), + cf_exe:continue(Call); + {undefined, CallflowId} -> + lager:debug("transfer instruction found"), + case couch_mgr:open_doc(whapps_call:account_db(Call), CallflowId) of + {ok, Doc} -> + lager:debug("branching to callflow ~s", [CallflowId]), + Flow = wh_json:get_value(<<"flow">>, Doc, wh_json:new()), + cf_exe:branch(Flow, Call); + {error, R} -> + lager:debug("could not branch to callflow ~s, ~p", [CallflowId, R]), + cf_exe:continue(Call) + end; + {BranchKey, _} -> + lager:debug("branch instruction found"), + cf_exe:attempt(BranchKey, Call) + end; + {_, _} -> + %% failure - attempt to fallback to non-speech enabled ivr + case wh_json:get_ne_value(<<"menu_id">>, Data, undefined) of + undefined -> + cf_exe:continue(Call); + MenuId -> + %% possibly say "due to high call volume, speech is currently disabled" + cf_menu:handle(wh_json:set_value(<<"id">>, MenuId, wh_json:new()), Call) + end + end. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Attempts to bridge to the the speech IVR +%% @end +%%-------------------------------------------------------------------- +-spec bridge_to_speech_ivr(wh_json:json_object(), whapps_call:call()) -> 'ok'. +bridge_to_speech_ivr(Data, Call) -> + SpeechIvrEndpoint = build_speech_ivr_endpoint(whapps_call:request_user(Call) + ,whapps_call:account_id(Call) + ,wh_json:get_value(<<"id">>, Data) + ,<<"PLACEHOLDER">> + ), + Timeout = whapps_config:get(?CONFIG_CAT, <<"bridge_timeout">>), + whapps_call_command:b_bridge([SpeechIvrEndpoint], Timeout, <<"simultaneous">>, true, Call). + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Builds the speech IVR endpoint +%% @end +%%-------------------------------------------------------------------- +build_speech_ivr_endpoint(Destination, AccountId, IvrId, CacheId) -> + Route = wh_util:to_binary([<<"sip:">>, Destination, <<"@">>, whapps_config:get(?CONFIG_CAT, <<"sip_uri_host">>)]), + SipHeaders = wh_json:from_list([{<<"X-Account-Id">>, AccountId} + ,{<<"X-IVR-Id">>, IvrId} + ,{<<"X-Cache-Id">>, CacheId} + ]), + wh_json:from_list([{<<"Invite-Format">>, <<"route">>} + ,{<<"Route">>, Route} + ,{<<"SIP-Headers">>, SipHeaders} + ]). diff --git a/whistle_apps/apps/crossbar/src/modules/cb_speech_ivr.erl b/whistle_apps/apps/crossbar/src/modules/cb_speech_ivr.erl new file mode 100644 index 00000000000..1b6a622945a --- /dev/null +++ b/whistle_apps/apps/crossbar/src/modules/cb_speech_ivr.erl @@ -0,0 +1,196 @@ +%%%------------------------------------------------------------------- +%%% @copyright (C) 2012, Umojo +%%% @doc +%%% Speech IVR module +%%% +%%% Handle client requests for Speech IVR callflow fragments +%%% +%%% @end +%%% @contributors +%%% Jon Blanton +%%%------------------------------------------------------------------- +-module(cb_speech_ivr). + +-export([init/0 + ,allowed_methods/0, allowed_methods/1 + ,resource_exists/0, resource_exists/1 + ,validate/1, validate/2 + ]). + +-include_lib("crossbar/include/crossbar.hrl"). + +-define(MOD_CONFIG_CAT, <<"speech_ivr">>). +-define(CB_LIST, <<"speech_ivr/crossbar_listing">>). +-define(DIRECTORY_LOOKUP, <<"speech_ivr/directory_lookup">>). +-define(EXPAND_KEYS, [<<"menu_id">>, <<"directory_id">>]). + +%%%=================================================================== +%%% API +%%%=================================================================== +init() -> + _ = crossbar_bindings:bind(<<"v1_resource.allowed_methods.speech_ivr">>, ?MODULE, allowed_methods), + _ = crossbar_bindings:bind(<<"v1_resource.resource_exists.speech_ivr">>, ?MODULE, resource_exists), + _ = crossbar_bindings:bind(<<"v1_resource.validate.speech_ivr">>, ?MODULE, validate), + crossbar_bindings:bind(<<"v1_resource.finish_request.*.speech_ivr">>, ?MODULE, reconcile_services). + +%%-------------------------------------------------------------------- +%% @public +%% @doc +%% This function determines the verbs that are appropriate for the +%% given Nouns. IE: '/accounts/' can only accept GET and PUT +%% +%% Failure here returns 405 +%% @end +%%-------------------------------------------------------------------- +-spec allowed_methods/0 :: () -> http_methods(). +allowed_methods() -> + ['GET']. + +-spec allowed_methods/1 :: (path_token()) -> http_methods(). +allowed_methods(_) -> + ['GET']. + +%%-------------------------------------------------------------------- +%% @public +%% @doc +%% This function determines if the provided list of Nouns are valid. +%% +%% Failure here returns 404 +%% @end +%%-------------------------------------------------------------------- +-spec resource_exists/0 :: () -> 'true'. +resource_exists() -> true. + +-spec resource_exists/1 :: (path_token()) -> 'true'. +resource_exists(_) -> true. + +%%-------------------------------------------------------------------- +%% @public +%% @doc +%% This function determines if the parameters and content are correct +%% for this request +%% +%% Failure here returns 400 +%% @end +%%-------------------------------------------------------------------- +-spec validate/1 :: (#cb_context{}) -> #cb_context{}. +validate(#cb_context{req_verb = <<"get">>}=Context) -> + load_speech_ivr_summary(Context). + +-spec validate/2 :: (#cb_context{}, path_token()) -> #cb_context{}. +validate(#cb_context{req_verb = <<"get">>}=Context, DocId) -> + load_speech_ivr(DocId, Context). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Attempt to load list of speech IVRs +%% @end +%%-------------------------------------------------------------------- +-spec load_speech_ivr_summary/1 :: (#cb_context{}) -> #cb_context{}. +load_speech_ivr_summary(Context) -> + crossbar_doc:load_view(?CB_LIST, [], Context, fun normalize_view_results_key/2). + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Load a speech IVR callfow fragment from the database, ids resolved +%% @end +%%-------------------------------------------------------------------- +-spec load_speech_ivr/2 :: (ne_binary(), #cb_context{}) -> #cb_context{}. +load_speech_ivr(DocId, Context) -> + case crossbar_doc:load_view(?CB_LIST, [{<<"key">>, DocId}], Context, fun normalize_view_results_value/2) of + #cb_context{resp_status=success, resp_data=[[Data, CallflowId] | _]}=Context1 -> + Data1 = wh_json:set_value(<<"callflow_id">>, CallflowId, Data), + lager:debug("expanding speech ivr data"), + expand_speech_ivr_data(Context1#cb_context{resp_data=Data1}); + %TODO: Expand directory here + Else -> + lager:debug("not expanding speech ivr data"), + Else + end. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Normalizes the resuts of a view using keys +%% @end +%%-------------------------------------------------------------------- +-spec expand_speech_ivr_data/1 :: (#cb_context{}) -> #cb_context{}. +expand_speech_ivr_data(#cb_context{resp_data=Data}=Context) -> + case collect_ids(Data) of + [_|_]=Ids -> + lager:debug("expanding 1 or more IDs"), + inject_docs(Ids, Context); + [] -> + lager:debug("no IDs found"), + Context + end. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Collects the value, specifically id, of particular fields +%% @end +%%-------------------------------------------------------------------- +-spec collect_ids/1 :: (wh_json:json_object()) -> [ne_binary(), ...] | []. +collect_ids(Data) -> + wh_json:foldl(fun(Key, Value, Acc) -> + case lists:member(Key, ?EXPAND_KEYS) of + true -> [Value | Acc]; + false -> Acc + end + end + ,[] + ,Data + ). + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Injects documents into the response data of a given context +%% @end +%%-------------------------------------------------------------------- +-spec inject_docs/2 :: ([ne_binary(), ...], #cb_context{}) -> #cb_context{}. +inject_docs(Ids, #cb_context{db_name=Db, resp_data=Data}=Context) -> + case couch_mgr:all_docs(Db, [{<<"keys">>, Ids}, {<<"include_docs">>, <<"true">>}]) of + {ok, Results} -> + lager:debug("got doc(s), injecting now"), + Data1 = lists:foldl(fun(Doc, Acc) -> + wh_json:set_value(wh_json:get_value(<<"pvt_type">>, Doc) + ,crossbar_doc:public_fields(Doc) + ,Acc + ) + end + ,Data + ,[wh_json:get_value(<<"doc">>, Result) || Result <- Results] + ), + Context#cb_context{resp_data=Data1}; + _Else -> + lager:debug("failed inject doc(s)"), + Context + end. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Normalizes the resuts of a view using keys +%% @end +%%-------------------------------------------------------------------- +-spec normalize_view_results_key/2 :: (wh_json:json_object(), wh_json:json_objects()) -> wh_json:json_objects(). +normalize_view_results_key(JObj, Acc) -> + [wh_json:get_value(<<"key">>, JObj)|Acc]. + +%%-------------------------------------------------------------------- +%% @private +%% @doc +%% Normalizes the resuts of a view using keys +%% @end +%%-------------------------------------------------------------------- +-spec normalize_view_results_value/2 :: (wh_json:json_object(), wh_json:json_objects()) -> wh_json:json_objects(). +normalize_view_results_value(JObj, Acc) -> + [wh_json:get_value(<<"value">>, JObj)|Acc].