%%%----------------------------------------------------------------------------- %%% @copyright (C) 2014-2019, 2600Hz %%% @doc A singular call is as an entire conversation as dialed by the caller, %%% and it may comprise of multiple "legs" or "calls". %%% %%% This module is called by {@link cf_exe} at the initialization and destroy %%% points of a call, in order to identify the conversation in a singular manner %%% and send out hooks to external URLs. %%% %%% Hook behavior is disabled by default, and will only be enabled if `singular_call_hook_url' %%% in the `system_config' or callflow configuration is populated. %%% %%% Example of JSON hook sent via `POST' on call init: %%% ``` %%% { %%% "Event": "init", %%% "CallID": "OTdlYzFkMDZlZmRhYWY1YmEzN2RhNmMxZWNiYTQ4NDc", %%% "To": "+14088317607", %%% "From": "+16505811111", %%% "Inception": "onnet" %%% } %%% ''' %%% %%% Example of JSON hook sent via `POST' on call destroy: %%% ``` %%% { %%% "Event": "destroy", %%% "CallID": "OTdlYzFkMDZlZmRhYWY1YmEzN2RhNmMxZWNiYTQ4NDc", %%% "To": "+14088317607", %%% "From": "+16505811111", %%% "Inception": "onnet", %%% "Duration-Seconds": "33", %%% "Hangup-Cause": "NORMAL_CLEARING", %%% "Disposition":"SUCCESS" %%% } %%% ''' %%% %%%
Be sure to set the internal and external caller IDs %%% for the devices. These are used to resolve to/from numbers correctly.
%%% %%% %%% @author Benedict Chan %%% @end %%%----------------------------------------------------------------------------- -module(cf_singular_call_hooks). -include("callflow.hrl"). %% API to send hooks -export([maybe_hook_call/1 ,send_init_hook/1 ,send_end_hook/2 ]). %% Helper functions -export([is_enabled/0 ,get_hook_url/0 ]). %%------------------------------------------------------------------------------ %% @doc First we check if this feature is enabled - if not, we return false. %% Next we check if the call is an indicator for the start of a singular call (A-leg), %% and if so, then we know the call should be hooked. %% We then can start the event listener, which will send init and end hooks. %% %% @end %%------------------------------------------------------------------------------ -spec maybe_hook_call(kapps_call:call()) -> boolean(). maybe_hook_call(Call) -> %% Never invoke anything if we are disabled case should_hook(Call) of 'true' -> %% start event listener, which will be responsible for sending all hooks, %% including the init hook (we want to do it this way to make sure we are listening) cf_exe:add_event_listener(Call, {'cf_singular_call_hooks_listener', []}); 'false' -> 'false' end. %%------------------------------------------------------------------------------ %% @doc Sends an initial request signifying the start of this entire conversation %% in a hook to a pre-configured URL. %% %% @end %%------------------------------------------------------------------------------ -spec send_init_hook(kapps_call:call()) -> boolean(). send_init_hook(Call) -> lager:debug("===CALL STARTED===", []), lager:debug("event: init", []), lager:debug("call-ID: ~s", [kapps_call:call_id_direct(Call)]), lager:debug("to: ~s", [knm_converters:normalize(kapps_call:to_user(Call))]), lager:debug("from: ~s", [knm_converters:normalize(kapps_call:caller_id_number(Call))]), lager:debug("inception: ~s", [get_inception(Call)]), lager:debug("================", []), JObj = kz_json:from_list( [{<<"Event">>, <<"init">>} ,{<<"Call-ID">>, kapps_call:call_id(Call)} ,{<<"To">>, knm_converters:normalize(kapps_call:to_user(Call))} ,{<<"From">>, knm_converters:normalize(kapps_call:caller_id_number(Call))} ,{<<"Inception">>, get_inception(Call)} ]), URI = binary_to_list(get_hook_url()), case kz_http:post(URI ,[{"Content-Type", "application/json"}] ,kz_json:encode(JObj) ,[{'connect_timeout', 5000}, {'timeout', 5000}] ) of {'error', Reason} -> lager:warning("error when sending singular call init hook: ~p", [Reason]), 'false'; _ -> 'true' end. %%------------------------------------------------------------------------------ %% @doc Sends a request signifying the end of this entire conversation in a %% hook to a pre-configured URL. %% %% @end %%------------------------------------------------------------------------------ -spec send_end_hook(kapps_call:call(), kz_json:object()) -> boolean(). send_end_hook(Call, Event) -> lager:debug("===CALL ENDED===", []), lager:debug("event: end", []), lager:debug("call-ID: ~s", [kapps_call:call_id_direct(Call)]), lager:debug("to: ~s", [knm_converters:normalize(kapps_call:to_user(Call))]), lager:debug("from: ~s", [knm_converters:normalize(kapps_call:caller_id_number(Call))]), lager:debug("inception: ~s", [get_inception(Call)]), lager:debug("callDuration: ~s", [kz_json:get_value(<<"Duration-Seconds">>, Event)]), lager:debug("hangupReason: ~s", [kz_json:get_value(<<"Hangup-Cause">>, Event)]), lager:debug("disposition: ~s", [kz_json:get_value(<<"Disposition">>, Event)]), lager:debug("================", []), ReferredBy = kapps_call:custom_channel_var(<<"Referred-By">>, Call), CallID = case ReferredBy of 'undefined' -> kapps_call:call_id_direct(Call); % if we were a forwarded call, refer to the original call id (bridge id) _ -> kapps_call:custom_channel_var(<<"Bridge-ID">>, Call) end, JObj = kz_json:from_list( [{<<"Event">>, <<"destroy">>} ,{<<"Call-ID">>, CallID} ,{<<"To">>, knm_converters:normalize(kapps_call:to_user(Call))} ,{<<"From">>, knm_converters:normalize(kapps_call:caller_id_number(Call))} ,{<<"Inception">>, get_inception(Call)} ,{<<"Duration-Seconds">>, kz_json:get_value(<<"Duration-Seconds">>, Event)} ,{<<"Hangup-Cause">>, kz_json:get_value(<<"Hangup-Cause">>, Event)} ,{<<"Disposition">>, kz_json:get_value(<<"Disposition">>, Event)} ]), URI = binary_to_list(get_hook_url()), case kz_http:post(URI ,[{"Content-Type", "application/json"}] ,kz_json:encode(JObj) ,[{'connect_timeout', 5000}, {'timeout', 5000}] ) of {'error', Reason} -> lager:warning("error when sending singular end of call hook: ~p", [Reason]), 'false'; _ -> 'true' end. %%------------------------------------------------------------------------------ %% @doc Checks if there is a non-empty hook URL and that the call is singular (or a transfer) %% @end %%------------------------------------------------------------------------------ -spec should_hook(kapps_call:call()) -> boolean(). should_hook(Call) -> is_enabled() andalso call_is_singular(Call). %%------------------------------------------------------------------------------ %% @doc Checks if the singular call hook is enabled in the callflow system config. %% The call hook is enabled if the URL in the `system_config' / callflows / `singular_call_hook_url' %% field is not set to disabled. %% %% @end %%------------------------------------------------------------------------------ -spec is_enabled() -> boolean(). is_enabled() -> (not kz_term:is_empty(get_hook_url())). %%------------------------------------------------------------------------------ %% @doc This function identifies if a call is the first of the conversation by %% checking if it has an existing bridge. We also check the presence of referred %% by and want to send the hook if it is a call transfer %% %% @end %%------------------------------------------------------------------------------ -spec call_is_singular(kapps_call:call()) -> boolean(). call_is_singular(Call) -> BridgeID = kapps_call:custom_channel_var(<<"Bridge-ID">>, Call), ReferredBy = kapps_call:custom_channel_var(<<"Referred-By">>, Call), CallID = kapps_call:call_id_direct(Call), (BridgeID =:= 'undefined') orelse (BridgeID =:= CallID) orelse (ReferredBy =/= 'undefined'). %%------------------------------------------------------------------------------ %% @doc Gets where the call was started. If kapps_call returns undefined, it was on net. %% @end %%------------------------------------------------------------------------------ -spec get_inception(kapps_call:call()) -> kz_term:ne_binary(). get_inception(Call) -> case kapps_call:inception(Call) of 'undefined' -> <<"onnet">>; _Else -> <<"offnet">> end. %%------------------------------------------------------------------------------ %% @doc Gets the singular call hook URL from the configuration (may be cached). %% @end %%------------------------------------------------------------------------------ -spec get_hook_url() -> kz_term:ne_binary(). get_hook_url() -> kapps_config:get_binary(?CF_CONFIG_CAT, <<"singular_call_hook_url">>, <<"">>).