%%%-----------------------------------------------------------------------------
%%% @copyright (C) 2011-2019, 2600Hz
%%% @doc Allow the user to change their Caller ID based on the action.
%%%
%%%
Data options:
%%%
%%% - `action'
%%% - How to collect Caller ID: `lists' (`list' has same effect), `static' or `manual'. Default is `manual'.
%%%
%%% - `media_id'
%%% - ID of the media prompt to play before starting collecting DTMF.
%%%
%%% - `id'
%%% - ID if the list document to use if action is `list' or `lists'. Required if the action is `list' or `lists'.
%%%
%%% - `interdigit'
%%% - Optional: How long to wait for the next DTMF, in milliseconds
%%%
%%%
%%% @author Karl Anderson
%%% @author William Lloyd
%%% @end
%%%-----------------------------------------------------------------------------
-module(cf_dynamic_cid).
-behaviour(gen_cf_action).
-include("callflow.hrl").
-export([handle/2]).
-define(MOD_CONFIG_CAT, <<(?CF_CONFIG_CAT)/binary, ".dynamic_cid">>).
-define(REJECT_PROMPT
,kapps_config:get_ne_binary(?MOD_CONFIG_CAT, <<"reject_prompt">>, <<"dynamic-cid-invalid_using_default">>)
).
-record(prompts
,{accept_tone =
kapps_config:get_ne_binary(?MOD_CONFIG_CAT, <<"accept_prompt">>, <<"tone_stream://%(250,50,440)">>)
:: kz_term:ne_binary()
,reject_tone = kz_media_util:get_prompt(?REJECT_PROMPT) :: kz_term:api_ne_binary()
,default_prompt =
kz_media_util:get_prompt(
kapps_config:get_ne_binary(?MOD_CONFIG_CAT, <<"default_prompt">>, <<"dynamic-cid-enter_cid">>)
)
:: kz_term:ne_binary()
}).
-type prompts() :: #prompts{}.
-record(dynamic_cid
,{prompts = #prompts{} :: prompts()
,default_max_digits = kapps_config:get_integer(?MOD_CONFIG_CAT, <<"max_digits">>, 10) :: integer()
,default_min_digits = kapps_config:get_integer(?MOD_CONFIG_CAT, <<"min_digits">>, 10) :: integer()
,default_whitelist = kapps_config:get_binary(?MOD_CONFIG_CAT, <<"whitelist_regex">>, <<"\\d+">>) :: kz_term:ne_binary()
}
).
-type cid() :: {kz_term:ne_binary(), kz_term:ne_binary()}.
%%------------------------------------------------------------------------------
%% @doc Entry point for this module
%% @end
%%------------------------------------------------------------------------------
-spec handle(kz_json:object(), kapps_call:call()) -> 'ok'.
handle(Data, Call) ->
CaptureGroup = kapps_call:kvs_fetch('cf_capture_group', Call),
Action = kz_json:get_ne_binary_value(<<"action">>, Data),
handle(Data, Call, Action, CaptureGroup).
-spec handle(kz_json:object(), kapps_call:call(), kz_term:api_ne_binary(), kz_term:api_ne_binary()) -> 'ok'.
handle(Data, Call, <<"list">>, ?NE_BINARY = _CaptureGroup) ->
lager:info("using account's lists/entries view to get new cid info"),
handle_list(Data, Call);
handle(Data, Call, <<"lists">>, ?NE_BINARY = _CaptureGroup) ->
lager:info("using account's lists/entries view to get new cid info"),
handle_lists(Data, Call);
handle(Data, Call, <<"static">>, CaptureGroup) ->
lager:info("user chose a static caller id for this call"),
handle_static(Data, Call, CaptureGroup);
handle(Data, Call, _Manual, ?NE_BINARY = CaptureGroup) ->
lager:info("user must manually enter on keypad the caller id for this call"),
handle_manual(Data, Call, CaptureGroup);
handle(Data, Call, _Manual, CaptureGroup) ->
lager:info("capture group is not present, forcing manual action. user must manually enter on keypad the caller id for this call"),
handle_manual(Data, Call, CaptureGroup).
%%------------------------------------------------------------------------------
%% @doc Handle manual mode of dynamic cid
%% @end
%%------------------------------------------------------------------------------
-spec handle_manual(kz_json:object(), kapps_call:call(), kz_term:api_ne_binary()) -> 'ok'.
handle_manual(Data, Call, CaptureGroup) ->
case collect_cid_number(Data, Call) of
{'ok', CIDNumber} ->
_NoopId = kapps_call_command:flush_dtmf(Call),
update_call_and_continue(Data, Call, CIDNumber, 'no_name', CaptureGroup, <<"manual">>);
{'error', 'channel_hungup'} ->
lager:info("caller hungup while collecting caller id number"),
cf_exe:stop(Call)
end.
%%------------------------------------------------------------------------------
%% @doc Handle static mode of dynamic cid
%% @end
%%------------------------------------------------------------------------------
-spec handle_static(kz_json:object(), kapps_call:call(), kz_term:api_ne_binary()) -> 'ok'.
handle_static(Data, Call, CaptureGroup) ->
{CIDName, CIDNumber} = get_static_cid_entry(Data, Call),
update_call_and_continue(Data, Call, CIDNumber, CIDName, CaptureGroup, <<"static">>).
%%------------------------------------------------------------------------------
%% @doc Read CID info from a list of CID defined in database
%% @end
%%------------------------------------------------------------------------------
-type cid_entry() :: {kz_term:ne_binary(), kz_term:ne_binary(), binary()}.
-type list_cid_entry() :: list_cid_entry() |
{'error', kz_datamgr:data_error()}.
-spec handle_list(kz_json:object(), kapps_call:call()) -> 'ok'.
handle_list(Data, Call) ->
maybe_proceed_with_call(Data, Call, get_list_entry(Data, Call)).
-spec handle_lists(kz_json:object(), kapps_call:call()) -> 'ok'.
handle_lists(Data, Call) ->
ListId = kz_doc:id(Data),
maybe_proceed_with_call(Data, Call, get_caller_id_from_entries(Call, ListId, 'undefined')).
-spec maybe_proceed_with_call(kz_json:object(), kapps_call:call(), list_cid_entry()) -> 'ok'.
maybe_proceed_with_call(Data, Call, {CIDName, CIDNumber, CaptureGroup}) ->
update_call_and_continue(Data, Call, CIDNumber, CIDName, CaptureGroup, <<"lists">>);
maybe_proceed_with_call(_Data, Call, {'error', _}) ->
lager:debug("failed to find cid name/number and destination from list(s), hanging up."),
cf_exe:stop_bad_destination(Call).
%%------------------------------------------------------------------------------
%% @doc Update caller id number. If call
%% has a capture group, strip the non capture group digits from
%% request, to and callee_number
%% @end
%%------------------------------------------------------------------------------
-spec update_call_and_continue(kz_json:object(), kapps_call:call(), kz_term:ne_binary(), kz_term:ne_binary() | 'no_name', kz_term:api_ne_binary(), kz_term:ne_binary()) -> 'ok'.
update_call_and_continue(Data, Call, CIDNumber, CIDName, CaptureGroup, Type) ->
Destination = cf_util:normalize_capture_group(CaptureGroup),
update_call(Call, CIDNumber, CIDName, Destination),
maybe_route_to_callflow(Data, Call, Destination, Type).
-spec update_call(kapps_call:call(), kz_term:ne_binary(), kz_term:ne_binary() | 'no_name', kz_term:api_ne_binary()) -> 'ok'.
update_call(Call, CIDNumber, 'no_name', Destination) ->
Updates = [{fun kapps_call:kvs_store/3, 'dynamic_cid', CIDNumber}
,{fun kapps_call:set_caller_id_number/2, CIDNumber}
],
{'ok', C1} = cf_exe:get_call(Call),
lager:info("setting the caller id number to ~s (from ~s)"
,[CIDNumber, kapps_call:caller_id_number(Call)]
),
maybe_strip_features_code(kapps_call:exec(Updates, C1), Destination);
update_call(Call, CIDNumber, CIDName, Destination) ->
Updates = [{fun kapps_call:kvs_store/3, 'dynamic_cid', {CIDNumber, CIDName}}
,{fun kapps_call:set_caller_id_number/2, CIDNumber}
,{fun kapps_call:set_caller_id_name/2, CIDName}
],
{'ok', C1} = cf_exe:get_call(Call),
lager:info("setting the cid to <~s> ~s (from <~s> ~s)"
,[CIDName
,CIDNumber
,kapps_call:caller_id_name(Call)
,kapps_call:caller_id_number(Call)
]
),
maybe_strip_features_code(kapps_call:exec(Updates, C1), Destination).
%%------------------------------------------------------------------------------
%% @doc If Destination exists correct "request", "to" and "callee_id_number"
%% @end
%%------------------------------------------------------------------------------
-spec maybe_strip_features_code(kapps_call:call(), kz_term:api_ne_binary()) -> 'ok'.
maybe_strip_features_code(Call, 'undefined') ->
cf_exe:set_call(Call);
maybe_strip_features_code(Call, Number) ->
Request = list_to_binary([Number, "@", kapps_call:request_realm(Call)]),
To = list_to_binary([Number, "@", kapps_call:to_realm(Call)]),
lager:info("sending the call onto real destination of: ~s", [Number]),
Updates = [{fun kapps_call:set_request/2, Request}
,{fun kapps_call:set_to/2, To}
,{fun kapps_call:set_callee_id_number/2, Number}
],
cf_exe:set_call(kapps_call:exec(Updates, Call)).
%%------------------------------------------------------------------------------
%% @doc Lookup callflow and continue with the call if we have a destination number
%%
%% @end
%%------------------------------------------------------------------------------
-spec maybe_route_to_callflow(kz_json:object(), kapps_call:call(), kz_term:api_ne_binary(), kz_term:ne_binary()) -> 'ok'.
maybe_route_to_callflow(_, Call, _, <<"manual">>) ->
cf_exe:continue(Call);
maybe_route_to_callflow(_, Call, _, <<"static">>) ->
cf_exe:continue(Call);
maybe_route_to_callflow(_, Call, 'undefined', <<"lists">>) ->
cf_exe:continue(Call);
maybe_route_to_callflow(Data, Call, Number, <<"lists">>) ->
case cf_flow:lookup(Number, kapps_call:account_id(Call)) of
{'ok', Flow, 'true'} ->
maybe_restrict_call(Data, Call, Number, Flow);
_ ->
lager:info("failed to find a callflow to satisfy ~s", [Number]),
_ = kapps_call_command:b_prompt(<<"disa-invalid_extension">>, Call),
cf_exe:stop(Call)
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec maybe_restrict_call(kz_json:object(), kapps_call:call(), kz_term:ne_binary(), kzd_callflows:doc()) -> 'ok'.
maybe_restrict_call(Data, Call, Number, Flow) ->
case should_restrict_call(Data, Call, Number) of
'true' ->
lager:info("not allowed to call this destination, terminate", []),
_ = kapps_call_command:answer(Call),
_ = kapps_call_command:prompt(<<"cf-unauthorized_call">>, Call),
_ = kapps_call_command:queued_hangup(Call),
'ok';
'false' ->
cf_exe:branch(kzd_callflows:flow(Flow), Call)
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec should_restrict_call(kz_json:object(), kapps_call:call(), kz_term:ne_binary()) ->
boolean().
should_restrict_call(Data, Call, Number) ->
case kz_json:is_true(<<"enforce_call_restriction">>, Data, 'true') of
'true' -> should_restrict_call(Call, Number);
'false' ->
lager:info("not enforcing call restrictions"),
'false'
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec should_restrict_call(kapps_call:call(), kz_term:ne_binary()) -> boolean().
should_restrict_call(Call, Number) ->
case kz_endpoint:get(Call) of
{'error', _} -> 'false';
{'ok', JObj} ->
Classification = knm_converters:classify(Number),
lager:info("classified number as ~s", [Classification]),
kz_json:get_ne_binary_value([<<"call_restriction">>, Classification, <<"action">>], JObj) =:= <<"deny">>
end.
%%------------------------------------------------------------------------------
%% @doc Collect CID number from user
%% @end
%%------------------------------------------------------------------------------
-spec collect_cid_number(kz_json:object(), kapps_call:call()) ->
{'ok', kz_term:ne_binary()} |
{'error', 'channel_hungup'}.
collect_cid_number(Data, Call) ->
DynamicCID = #dynamic_cid{},
Prompts = DynamicCID#dynamic_cid.prompts,
_ = kapps_call_command:b_play(<<"silence_stream://100">>, Call),
Media = case kz_json:get_ne_binary_value(<<"media_id">>, Data) of
'undefined' -> Prompts#prompts.default_prompt;
Else -> Else
end,
DefaultMin = DynamicCID#dynamic_cid.default_min_digits,
DefaultMax = DynamicCID#dynamic_cid.default_max_digits,
DefaultRegex = DynamicCID#dynamic_cid.default_whitelist,
DefaultCID = kapps_call:caller_id_number(Call),
Min = kz_json:get_integer_value(<<"min_digits">>, Data, DefaultMin),
Max = kz_json:get_integer_value(<<"max_digits">>, Data, DefaultMax),
Regex = kz_json:get_ne_binary_value(<<"whitelist_regex">>, Data, DefaultRegex),
Interdigit = kz_json:get_integer_value(<<"interdigit_timeout">>
,Data
,kapps_call_command:default_interdigit_timeout()
),
NoopId = kapps_call_command:play(Media, Call),
CollectTimeout = kapps_call_command:default_collect_timeout(),
case kapps_call_command:collect_digits(Max, CollectTimeout, Interdigit, NoopId, Call) of
{'ok', <<>>} ->
_ = kapps_call_command:play(Prompts#prompts.reject_tone, Call),
{'ok', DefaultCID};
{'ok', Digits} ->
case re:run(Digits, Regex) of
{'match', _} when byte_size(Digits) >= Min ->
_NoopId = kapps_call_command:play(Prompts#prompts.accept_tone, Call),
{'ok', Digits};
_ ->
_ = kapps_call_command:play(Prompts#prompts.reject_tone, Call),
{'ok', DefaultCID}
end;
{'error', 'channel_hungup'} ->
{'error', 'channel_hungup'};
{'error', _} ->
_ = kapps_call_command:play(Prompts#prompts.reject_tone, Call),
{'ok', DefaultCID}
end.
%%------------------------------------------------------------------------------
%% @doc Get static CID from callflow data
%% @end
%%------------------------------------------------------------------------------
-spec get_static_cid_entry(kz_json:object(), kapps_call:call()) -> cid().
get_static_cid_entry(Data, Call) ->
case kz_json:get_json_value(<<"caller_id">>, Data) of
'undefined' ->
maybe_set_default_cid('undefined', 'undefined', Call);
NewCallerId ->
Name = kz_json:get_ne_binary_value(<<"name">>, NewCallerId),
Number = kz_json:get_ne_binary_value(<<"number">>, NewCallerId),
maybe_set_default_cid(Name, Number, Call)
end.
%%------------------------------------------------------------------------------
%% @doc Pull in document from database with the caller id switching information inside
%% @end
%%------------------------------------------------------------------------------
-type key_dest() :: 'undefined' | {kz_term:ne_binary(), binary()}.
-spec get_list_entry(kz_json:object(), kapps_call:call()) -> list_cid_entry().
get_list_entry(Data, Call) ->
ListId = kz_json:get_ne_binary_value(<<"id">>, Data),
get_caller_id_from_entries(Call, ListId, maybe_key_and_dest_using_data(Data, Call)).
-spec maybe_key_and_dest_using_data(kz_json:object(), kapps_call:call()) -> key_dest().
maybe_key_and_dest_using_data(Data, Call) ->
case kz_json:get_ne_binary_value(<<"idx_name">>, Data) of
'undefined' -> 'undefined';
Idx ->
Groups = kapps_call:kvs_fetch('cf_capture_groups', Call),
CIDKey = kz_json:get_ne_binary_value(Idx, Groups),
Destination = kapps_call:kvs_fetch('cf_capture_group', Call),
{CIDKey, Destination}
end.
-spec get_caller_id_from_entries(kapps_call:call(), kz_term:api_ne_binary(), key_dest()) ->
list_cid_entry().
get_caller_id_from_entries(_Call, 'undefined', _KeyDest) ->
lager:warning("list id is missing"),
{'error', 'not_found'};
get_caller_id_from_entries(Call, ListId, KeyDest) ->
case kz_datamgr:get_results(kapps_call:account_db(Call), <<"lists/entries">>, [{'key', ListId}]) of
{'ok', Entries} ->
lager:debug("trying to find new caller id from ~b list entries", [length(Entries)]),
get_new_caller_id(Call, Entries, ListId, KeyDest);
{'error', _Reason}=Error ->
lager:info("failed to load entry documents ~s: ~p", [ListId, _Reason]),
Error
end.
-spec get_new_caller_id(kapps_call:call(), kz_json:objects(), kz_term:ne_binary(), key_dest()) ->
list_cid_entry().
get_new_caller_id(Call, [], _ListId, {_, Destination}) ->
lager:warning("no entries were found in list ~p", [_ListId]),
{CidName, CidNumber} = maybe_set_default_cid('undefined', 'undefined', Call),
{CidName, CidNumber, Destination};
get_new_caller_id(Call, [], ListId, 'undefined') ->
lager:warning("no entries were found, maybe finding destination number using specified index"),
LengthDigits = get_cid_length_from_list_document(Call, ListId),
CaptureGroup = kapps_call:kvs_fetch('cf_capture_group', Call),
captured_key_and_destination(LengthDigits, CaptureGroup);
get_new_caller_id(Call, [JObj | Entries], ListId, KeyDest) ->
Entry = kz_json:get_json_value(<<"value">>, JObj),
case get_key_and_dest(Call, Entry, KeyDest) of
{'error', _}=Error -> Error;
{CIDKey, Destination} ->
case get_entry_caller_id(Call, Entry, CIDKey, Destination) of
'undefined' -> get_new_caller_id(Call, Entries, ListId, KeyDest);
{_, _, _}=CID -> CID
end
end.
-spec get_entry_caller_id(kapps_call:call(), kz_json:object(), kz_term:ne_binary(), binary()) ->
cid_entry() | 'undefined'.
get_entry_caller_id(Call, Entry, CIDKey, Destination) ->
case kz_json:get_ne_binary_value(<<"capture_group_key">>, Entry) of
CIDKey ->
Name = kz_json:get_ne_binary_value(<<"name">>, Entry),
Number = kz_json:get_ne_binary_value(<<"number">>, Entry),
{CidName, CidNumber} = maybe_set_default_cid(Name, Number, Call),
{CidName, CidNumber, Destination};
_Key -> 'undefined'
end.
-spec get_key_and_dest(kapps_call:call(), kz_json:object(), key_dest()) ->
key_dest() |
{'error', kz_term:ne_binary() | atom()}.
get_key_and_dest(_Call, _Entry, {CIDKey, _}=KeyDest) ->
case not kz_term:is_ne_binary(CIDKey) of
'true' -> KeyDest;
'false' -> {'error', <<"key_dest_failed">>}
end;
get_key_and_dest(Call, Entry, 'undefined') ->
LengthDigits = kz_json:get_integer_value(<<"capture_group_length">>, Entry, 2),
CaptureGroup = kapps_call:kvs_fetch('cf_capture_group', Call),
captured_key_and_destination(LengthDigits, CaptureGroup).
-spec captured_key_and_destination(non_neg_integer(), kz_term:ne_binary()) ->
key_dest() |
{'error', 'not_found'}.
captured_key_and_destination(LengthDigits, CaptureGroup) when byte_size(CaptureGroup) >= LengthDigits ->
<> = CaptureGroup,
{CIDKey, Destination};
captured_key_and_destination(_LengthDigits, _CaptureGroup) ->
lager:warning("failed to get cid_key (with length ~b) from capture group '~s'"
,[_LengthDigits, _CaptureGroup]
),
{'error', 'not_found'}.
-spec get_cid_length_from_list_document(kapps_call:call(), kz_term:ne_binary()) -> non_neg_integer().
get_cid_length_from_list_document(Call, ListId) ->
case kz_datamgr:open_cache_doc(kapps_call:account_db(Call), ListId) of
{'ok', ListJObj} ->
case kz_json:get_integer_value(<<"length">>, ListJObj, 2) of
I when is_integer(I), I > 0 -> I;
_Length ->
lager:info("cid length from ~s is less than '1', using default '2'", [ListId]),
2
end;
{'error', _Reason} ->
lager:info("failed to load list document ~s; using default length '2': ~p", [ListId, _Reason]),
2
end.
%%------------------------------------------------------------------------------
%% @doc Play reject prompt if any of the caller id are empty
%% @end
%%------------------------------------------------------------------------------
-spec maybe_set_default_cid(kz_term:api_ne_binary(), kz_term:api_ne_binary(), kapps_call:call()) -> cid().
maybe_set_default_cid('undefined', 'undefined', Call) ->
lager:debug("empty cid entry, set to default value"),
play_reject_prompt(Call),
{kapps_call:caller_id_name(Call), kapps_call:caller_id_number(Call)};
maybe_set_default_cid('undefined', Number, Call) ->
lager:debug("empty cid name, set to default value"),
{kapps_call:caller_id_name(Call), Number};
maybe_set_default_cid(Name, 'undefined', Call) ->
lager:debug("empty cid number, set to default value"),
play_reject_prompt(Call),
{Name, kapps_call:caller_id_number(Call)};
maybe_set_default_cid(Name, Number, _Call) ->
{Name, Number}.
%%------------------------------------------------------------------------------
%% @doc play reject prompts when caller id number is empty or invalid
%% @end
%%------------------------------------------------------------------------------
-spec play_reject_prompt(kapps_call:call()) -> 'ok'.
play_reject_prompt(Call) ->
_ = kapps_call_command:play(kapps_call:get_prompt(Call, ?REJECT_PROMPT), Call),
'ok'.