%%%-----------------------------------------------------------------------------
%%% @copyright (C) 2011-2019, 2600Hz
%%% @doc Check/compose Voicemail messages.
%%%
%%%
Data options:
%%%
%%% - `action'
%%% - One of: `compose' or `check'.
%%%
%%% - `id'
%%% - Optional: Voice Mailbox ID.
%%%
%%% - `max_message_length'
%%% - Optional: Maximum length of the voicemail message. Default is 500.
%%%
%%% - `interdigit_timeout'
%%% - How long to wait for the next DTMF, in milliseconds. Default is 2000.
%%%
%%%
%%% @author Karl Anderson
%%% @author James Aimonetti
%%% @end
%%%-----------------------------------------------------------------------------
-module(cf_voicemail).
-behaviour(gen_cf_action).
-include("callflow.hrl").
-include_lib("kazoo_stdlib/include/kazoo_json.hrl").
-export([handle/2]).
-export([new_message/4]).
-define(KEY_VOICEMAIL, <<"voicemail">>).
-define(KEY_MAX_MESSAGE_COUNT, <<"max_message_count">>).
-define(KEY_MAX_MESSAGE_LENGTH, <<"max_message_length">>).
-define(KEY_MIN_MESSAGE_SIZE, <<"min_message_size">>).
-define(KEY_MAX_BOX_NUMBER_LENGTH, <<"max_box_number_length">>).
-define(KEY_EXTENSION, <<"extension">>).
-define(KEY_MAX_LOGIN_ATTEMPTS, <<"max_login_attempts">>).
-define(KEY_MAX_PIN_LENGTH, <<"max_pin_length">>).
-define(KEY_DELETE_AFTER_NOTIFY, <<"delete_after_notify">>).
-define(KEY_SAVE_AFTER_NOTIFY, <<"save_after_notify">>).
-define(KEY_FORCE_REQUIRE_PIN, <<"force_require_pin">>).
-define(MAX_INVALID_PIN_LOOPS, 3).
-define(MAILBOX_DEFAULT_SIZE
,kapps_config:get_integer(?CF_CONFIG_CAT
,[?KEY_VOICEMAIL, ?KEY_MAX_MESSAGE_COUNT]
,100
)).
-define(MAILBOX_DEFAULT_MSG_MAX_LENGTH
,kapps_config:get_integer(?CF_CONFIG_CAT
,[?KEY_VOICEMAIL, ?KEY_MAX_MESSAGE_LENGTH]
,500
)).
-define(MAILBOX_DEFAULT_MSG_MIN_LENGTH
,kapps_config:get_integer(?CF_CONFIG_CAT
,[?KEY_VOICEMAIL, ?KEY_MIN_MESSAGE_SIZE]
,500
)).
-define(MAILBOX_DEFAULT_BOX_NUMBER_LENGTH
,kapps_config:get_integer(?CF_CONFIG_CAT
,[?KEY_VOICEMAIL, ?KEY_MAX_BOX_NUMBER_LENGTH]
,15
)).
-define(MAX_LOGIN_ATTEMPTS
,kapps_config:get_integer(?CF_CONFIG_CAT
,[?KEY_VOICEMAIL, ?KEY_MAX_LOGIN_ATTEMPTS]
,3
)).
-define(DEFAULT_MAX_PIN_LENGTH
,kapps_config:get_integer(?CF_CONFIG_CAT
,[?KEY_VOICEMAIL, ?KEY_MAX_PIN_LENGTH]
,6
)).
-define(DEFAULT_SAVE_AFTER_NOTIFY
,kapps_config:get_is_true(?CF_CONFIG_CAT
,[?KEY_VOICEMAIL, ?KEY_SAVE_AFTER_NOTIFY]
,'false'
)).
-define(DEFAULT_DELETE_AFTER_NOTIFY
,kapps_config:get_is_true(?CF_CONFIG_CAT
,[?KEY_VOICEMAIL, ?KEY_DELETE_AFTER_NOTIFY]
,'false'
)).
-define(DEFAULT_FORWARD_TYPE
,kapps_config:get_ne_binary(?CF_CONFIG_CAT
,[?KEY_VOICEMAIL, <<"vm_message_forward_type">>]
,<<"only_forward">>
)).
-define(FORCE_REQUIRE_PIN
,kapps_config:get_is_true(?CF_CONFIG_CAT
,[?KEY_VOICEMAIL, ?KEY_FORCE_REQUIRE_PIN]
,'false'
)
).
-define(DEFAULT_FIND_BOX_PROMPT, <<"vm-enter_id">>).
-record(keys, {operator = <<"0">> :: kz_term:ne_binary()
%% Compose Voicemail
,login = <<"*">> :: kz_term:ne_binary()
%% Record Review
,save = <<"1">> :: kz_term:ne_binary()
,listen = <<"2">> :: kz_term:ne_binary()
,record = <<"3">> :: kz_term:ne_binary()
%% Main Menu
,hear_new = <<"1">> :: kz_term:ne_binary()
,hear_saved = <<"2">> :: kz_term:ne_binary()
,configure = <<"5">> :: kz_term:ne_binary()
,exit = <<"#">> :: kz_term:ne_binary()
%% Config Menu
,rec_unavailable = <<"1">> :: kz_term:ne_binary()
,rec_name = <<"2">> :: kz_term:ne_binary()
,set_pin = <<"3">> :: kz_term:ne_binary()
,rec_temporary_unavailable = <<"4">> :: kz_term:ne_binary()
,del_temporary_unavailable = <<"5">> :: kz_term:ne_binary()
,return_main = <<"0">> :: kz_term:ne_binary()
%% Post playback
,keep = <<"1">> :: kz_term:ne_binary()
,replay = <<"2">> :: kz_term:ne_binary()
,forward = <<"3">> :: kz_term:ne_binary()
,prev = <<"4">> :: kz_term:ne_binary()
,next = <<"6">> :: kz_term:ne_binary()
,delete = <<"7">> :: kz_term:ne_binary()
%% Greeting or instructions
,continue = 'undefined' :: kz_term:api_ne_binary()
}).
-type vm_keys() :: #keys{}.
-define(KEY_LENGTH, 1).
-record(mailbox, {mailbox_id :: kz_term:api_ne_binary()
,mailbox_number = <<>> :: binary()
,exists = 'false' :: boolean()
,skip_instructions = 'false' :: boolean()
,skip_greeting = 'false' :: boolean()
,skip_envelope = 'false' :: boolean()
,unavailable_media_id :: kz_term:api_ne_binary()
,temporary_unavailable_media_id :: kz_term:api_ne_binary()
,name_media_id :: kz_term:api_ne_binary()
,pin = <<>> :: binary()
,timezone :: kz_term:api_ne_binary()
,max_login_attempts = ?MAX_LOGIN_ATTEMPTS :: non_neg_integer()
,require_pin = 'false' :: boolean()
,check_if_owner = 'true' :: boolean()
,owner_id :: kz_term:api_ne_binary()
,is_setup = 'false' :: boolean()
,message_count = 0 :: non_neg_integer()
,max_message_count = 0 :: non_neg_integer()
,max_message_length = ?MAILBOX_DEFAULT_MSG_MAX_LENGTH :: pos_integer()
,min_message_length = ?MAILBOX_DEFAULT_MSG_MIN_LENGTH :: pos_integer()
,keys = #keys{} :: vm_keys()
,transcribe_voicemail = 'false' :: boolean()
,notifications :: kz_term:api_object()
,after_notify_action = 'nothing' :: 'nothing' | 'delete' | 'save'
,interdigit_timeout = kapps_call_command:default_interdigit_timeout() :: pos_integer()
,play_greeting_intro = 'false' :: boolean()
,use_person_not_available = 'false' :: boolean()
,not_configurable = 'false' :: boolean()
,account_db :: kz_term:api_ne_binary()
,media_extension :: kz_term:api_ne_binary()
,forward_type :: kz_term:api_ne_binary()
}).
-type mailbox() :: #mailbox{}.
%%------------------------------------------------------------------------------
%% @doc Entry point for this module, based on the payload will either
%% connect a caller to check_voicemail or compose_voicemail.
%% @end
%%------------------------------------------------------------------------------
-spec handle(kz_json:object(), kapps_call:call()) -> 'ok'.
handle(Data, Call) ->
case kz_json:get_ne_binary_value(<<"action">>, Data, <<"compose">>) of
<<"compose">> ->
kapps_call_command:answer(Call),
lager:debug("answered the call and composing the voicemail"),
case compose_voicemail(get_mailbox_profile(Data, Call), Call) of
'ok' ->
lager:info("compose voicemail complete"),
cf_exe:continue(Call);
{'branch', Flow} ->
lager:info("compose voicemail complete, branch to operator"),
cf_exe:branch(Flow, Call);
{'error', 'channel_hungup'} ->
lager:info("channel has hungup, stopping the compose"),
cf_exe:stop(Call)
end;
<<"check">> ->
kapps_call_command:answer(Call),
case check_mailbox(get_mailbox_profile(Data, Call), Call) of
'ok' -> cf_exe:continue(Call);
{'error', 'channel_hungup'} -> cf_exe:stop(Call)
end;
_ ->
cf_exe:continue(Call)
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec check_mailbox(mailbox(), kapps_call:call()) ->
'ok' | {'error', 'channel_hungup'}.
check_mailbox(Box, Call) ->
%% Wrapper to initialize the attempt counter
Resp = check_mailbox(Box, Call, 1),
_ = send_mwi_update(Box),
Resp.
-spec check_mailbox(mailbox(), kapps_call:call(), non_neg_integer()) ->
'ok' | {'error', 'channel_hungup'}.
check_mailbox(#mailbox{owner_id=OwnerId}=Box, Call, Loop) ->
IsOwner = is_owner(Call, OwnerId),
check_mailbox(Box, IsOwner, Call, Loop).
-spec check_mailbox(mailbox(), boolean(), kapps_call:call(), non_neg_integer()) ->
'ok' | {'error', 'channel_hungup'}.
check_mailbox(#mailbox{max_login_attempts=MaxLoginAttempts}, _, Call, Loop) when Loop > MaxLoginAttempts ->
%% if we have exceeded the maximum loop attempts then terminate this call
lager:info("maximum number of invalid attempts to check mailbox"),
_ = kapps_call_command:b_prompt(<<"vm-abort">>, Call),
'ok';
check_mailbox(#mailbox{exists='false'
,max_login_attempts=MaxLoginAttempts
}=Box, _ , Call, Loop) ->
%% if the callflow did not define the mailbox to check then request the mailbox ID from the user
case find_mailbox(Box, Call, ?DEFAULT_FIND_BOX_PROMPT, Loop) of
{'ok', PossibleBox, NewLoop} -> check_mailbox(PossibleBox, Call, NewLoop);
{'error', 'not_found'} ->
%% can't find mailbox, set Loop to max to play abort prompts in above function clause
check_mailbox(Box, Call, MaxLoginAttempts + 1)
end;
check_mailbox(#mailbox{is_setup='false'}=Box, 'true', Call, _) ->
%% If this is the owner of the mailbox calling in and it is not setup then jump
%% right to the setup wizard
lager:info("caller is the owner of this mailbox, and it has not been setup yet"),
main_menu(Box, Call);
check_mailbox(#mailbox{require_pin='false'}=Box, 'true', Call, _) ->
%% If this is the owner of the mailbox calling in and it doesn't require a pin then jump
%% right to the main menu
lager:info("caller is the owner of this mailbox, and requires no pin"),
main_menu(Box, Call);
check_mailbox(#mailbox{pin = <<>>}, _, Call, _) ->
%% If the caller is not the owner or the owner with require pin set but the voicemail box
%% has no pin set then terminate this call.
lager:info("attempted to sign into a mailbox with no pin"),
_ = kapps_call_command:b_prompt(<<"vm-no_access">>, Call),
'ok';
check_mailbox(#mailbox{pin=Pin
,interdigit_timeout=Interdigit
}=Box, IsOwner, Call, Loop) ->
lager:info("requesting pin number to check mailbox"),
NoopId = kapps_call_command:prompt(<<"vm-enter_pass">>, Call),
case kapps_call_command:collect_digits(?DEFAULT_MAX_PIN_LENGTH
,kapps_call_command:default_collect_timeout()
,Interdigit
,NoopId
,Call
)
of
{'ok', Pin} ->
lager:info("caller entered a valid pin"),
main_menu(Box, Call);
{'ok', _} ->
lager:info("invalid mailbox login"),
_ = kapps_call_command:b_prompt(<<"vm-fail_auth">>, Call),
check_mailbox(Box, IsOwner, Call, Loop + 1);
_ ->
'ok'
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec find_mailbox(mailbox(), kapps_call:call(), kz_term:ne_binary(), non_neg_integer()) ->
{'ok', mailbox(), non_neg_integer()} |
{'error', 'not_found'}.
find_mailbox(#mailbox{max_login_attempts=MaxLoginAttempts}, _Call, _VmEntryIdMedia, Loop)
when Loop > MaxLoginAttempts ->
%% if we have exceeded the maximum loop attempts then terminate this call
%% Note: caller should play vm-abort or appropriate abort failure
lager:info("maximum number of invalid attempts to find mailbox"),
{'error', 'not_found'};
find_mailbox(#mailbox{interdigit_timeout=Interdigit}=Box, Call, VmEntryIdMedia, Loop) ->
lager:info("requesting mailbox number to check"),
NoopId = kapps_call_command:prompt(VmEntryIdMedia, Call),
case kapps_call_command:collect_digits(?MAILBOX_DEFAULT_BOX_NUMBER_LENGTH
,kapps_call_command:default_collect_timeout()
,Interdigit
,NoopId
,Call
)
of
{'ok', <<>>} ->
_ = kapps_call_command:b_prompt(<<"menu-invalid_entry">>, Call),
find_mailbox(Box, Call, VmEntryIdMedia, Loop + 1);
{'ok', Mailbox} ->
BoxNum = try kz_term:to_integer(Mailbox) catch _:_ -> 0 end,
case find_mailbox_by_number(BoxNum, Call) of
{'ok', FoundBox} -> {'ok', FoundBox, Loop};
{'error', 'not_found'} ->
_ = kapps_call_command:b_prompt(<<"menu-invalid_entry">>, Call),
find_mailbox(Box, Call, VmEntryIdMedia, Loop + 1);
{'error', _R} ->
lager:info("mailbox ~s lookup failed: ~p", [Mailbox, _R]),
_ = kapps_call_command:b_prompt(<<"menu-invalid_entry">>, Call),
find_mailbox(Box, Call, VmEntryIdMedia, Loop + 1)
end;
_E ->
lager:info("recv other: ~p", [_E]),
{'error', 'not_found'}
end.
%%------------------------------------------------------------------------------
%% @doc Find the voicemail box, by making a fake 'callflow data payload' we look
%% for it now because if the caller is the owner, and the pin is not required
%% then we skip requesting the pin.
%%
%% Check mailbox existence here to properly updating `Loop' in
%% {@link find_mailbox/4}.
%% @end
%%------------------------------------------------------------------------------
-spec find_mailbox_by_number(non_neg_integer(), kapps_call:call()) ->
{'ok', mailbox()} |
{'error', any()}.
find_mailbox_by_number(BoxNum, Call) ->
ViewOptions = [{'key', BoxNum}],
AccountDb = kapps_call:account_db(Call),
case kz_datamgr:get_single_result(AccountDb, <<"vmboxes/listing_by_mailbox">>, ViewOptions) of
{'ok', JObj} ->
lager:info("get profile of ~p", [JObj]),
case get_mailbox_profile(kz_json:from_list([{<<"id">>, kz_doc:id(JObj)}]), Call) of
#mailbox{exists='false'} -> {'error', 'not_found'};
Found -> {'ok', Found}
end;
Error -> Error
end.
-spec find_destination_mailbox(mailbox(), kapps_call:call(), kz_term:ne_binary(), non_neg_integer()) -> mailbox().
find_destination_mailbox(#mailbox{max_login_attempts=MaxLoginAttempts}, Call, _SrcBoxId, Loop)
when Loop > MaxLoginAttempts ->
lager:info("maximum number of invalid attempts to find destination mailbox"),
_ = kapps_call_command:b_prompt(<<"vm-forward_abort">>, Call),
#mailbox{};
find_destination_mailbox(#mailbox{max_login_attempts=MaxLoginAttempts}=Box, Call, SrcBoxId, Loop) ->
case find_mailbox(#mailbox{}, Call, <<"vm-enter_forward_id">>, Loop) of
{'ok', #mailbox{mailbox_id=SrcBoxId}, NewLoop} ->
lager:info("source mailbox can't be a destination mailbox"),
_ = kapps_call_command:b_prompt(<<"menu-invalid_entry">>, Call),
find_destination_mailbox(Box, Call, SrcBoxId, NewLoop + 1);
{'ok', DestBox, _NewLoop} -> DestBox;
{'error', 'not_found'} ->
%% can't find mailbox, set Loop to max to play abort prompts in above function clause
find_destination_mailbox(Box, Call, SrcBoxId, MaxLoginAttempts + 1)
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-type compose_return() :: 'ok' |
{'branch', _} |
{'error', 'channel_hungup'}.
-spec compose_voicemail(mailbox(), kapps_call:call()) -> compose_return().
compose_voicemail(#mailbox{owner_id=OwnerId}=Box, Call) ->
IsOwner = is_owner(Call, OwnerId),
compose_voicemail(Box, IsOwner, Call).
-spec compose_voicemail(mailbox(), boolean(), kapps_call:call()) -> compose_return().
compose_voicemail(#mailbox{check_if_owner='true'}=Box, 'true', Call) ->
lager:info("caller is the owner of this mailbox"),
lager:info("overriding action as check (instead of compose)"),
check_mailbox(Box, Call);
compose_voicemail(#mailbox{exists='false'}, _IsOwner, Call) ->
lager:info("attempted to compose voicemail for missing mailbox"),
_ = kapps_call_command:b_prompt(<<"vm-not_available_no_voicemail">>, Call),
'ok';
compose_voicemail(#mailbox{max_message_count=MaxCount
,message_count=Count
}=Box
,_IsOwner
,Call
)
when Count >= MaxCount
andalso MaxCount > 0 ->
handle_full_mailbox(Box, Call);
compose_voicemail(Box, _IsOwner, Call) ->
start_composing_voicemail(Box, Call).
-spec start_composing_voicemail(mailbox(), kapps_call:call()) -> compose_return().
start_composing_voicemail(#mailbox{media_extension=Ext}=Box, Call) ->
lager:debug("playing mailbox greeting to caller"),
_ = play_greeting_intro(Box, Call),
_ = play_greeting(Box, Call),
_ = play_instructions(Box, Call),
_NoopId = kapps_call_command:noop(Call),
%% timeout after 5 min for safety, so this process cant hang around forever
case kapps_call_command:wait_for_application_or_dtmf(<<"noop">>, 300000) of
{'ok', _} ->
lager:info("played greeting and instructions to caller, recording new message"),
record_voicemail(tmp_file(Ext), Box, Call);
{'dtmf', Digit} ->
_ = kapps_call_command:b_flush(Call),
handle_compose_dtmf(Box, Call, Digit);
{'error', R} ->
lager:info("error while playing voicemail greeting: ~p", [R])
end.
-spec handle_compose_dtmf(mailbox(), kapps_call:call(), kz_term:ne_binary()) -> compose_return().
handle_compose_dtmf(#mailbox{keys=#keys{login=Login}}=Box, Call, Login) ->
lager:info("caller pressed '~s', redirecting to check voicemail", [Login]),
check_mailbox(Box, Call);
handle_compose_dtmf(#mailbox{media_extension=Ext
,keys=#keys{operator=Operator}
}=Box
,Call
,Operator
) ->
lager:info("caller chose to ring the operator"),
case cf_util:get_operator_callflow(kapps_call:account_id(Call)) of
{'ok', Flow} -> {'branch', Flow};
{'error', _R} -> record_voicemail(tmp_file(Ext), Box, Call)
end;
handle_compose_dtmf(#mailbox{keys=#keys{continue=Continue}}=_Box, _Call, Continue) ->
lager:info("caller chose to continue to the next element in the callflow");
handle_compose_dtmf(#mailbox{media_extension=Ext}=Box, Call, _DTMF) ->
lager:info("caller pressed unbound '~s', skip to recording new message", [_DTMF]),
record_voicemail(tmp_file(Ext), Box, Call).
-spec handle_full_mailbox(mailbox(), kapps_call:call()) ->
'ok' | {'error', 'channel_hungup'}.
handle_full_mailbox(#mailbox{mailbox_id=VMBId
,keys=#keys{login=Login}
,max_message_count=MaxCount
,message_count=Count
}=Box, Call) ->
lager:debug("voicemail box is full, cannot hold more messages, sending notification"),
Props = [{<<"Account-ID">>, kapps_call:account_id(Call)}
,{<<"Voicemail-Box">>, VMBId}
,{<<"Max-Message-Count">>, MaxCount}
,{<<"Message-Count">>, Count}
| kz_api:default_headers(?APP_NAME, ?APP_VERSION)
],
kapps_notify_publisher:cast(Props, fun kapi_notifications:publish_voicemail_full/1),
lager:debug("playing mailbox greeting to caller"),
_ = play_greeting_intro(Box, Call),
_ = play_greeting(Box, Call),
_ = kapps_call_command:prompt(<<"vm-mailbox_full">>, Call),
_NoopId = kapps_call_command:noop(Call),
case kapps_call_command:wait_for_application_or_dtmf(<<"noop">>, 5 * ?MILLISECONDS_IN_MINUTE) of
{'dtmf', Login} ->
lager:info("caller wishes to login to mailbox"),
check_mailbox(Box, Call);
_Else ->
lager:debug("finished with call")
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec play_greeting_intro(mailbox(), kapps_call:call()) -> kz_term:ne_binary() | 'ok'.
play_greeting_intro(#mailbox{play_greeting_intro='true'}, Call) ->
kapps_call_command:audio_macro([{'prompt', <<"vm-greeting_intro">>}], Call);
play_greeting_intro(_, _) -> 'ok'.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec play_greeting(mailbox(), kapps_call:call()) -> kz_term:ne_binary() | 'ok'.
play_greeting(#mailbox{skip_greeting='true'}, _Call) -> 'ok';
play_greeting(#mailbox{temporary_unavailable_media_id= <<_/binary>> = MediaId}
,Call
) ->
Corrected = kz_media_util:media_path(MediaId, kapps_call:account_id(Call)),
lager:info("mailbox has a temporary greeting which always overrides standard greeting: '~s', corrected to '~s'",
[MediaId, Corrected]
),
kapps_call_command:play(Corrected, Call);
play_greeting(#mailbox{use_person_not_available='true'
,unavailable_media_id='undefined'
}, Call) ->
lager:debug("mailbox has no greeting, playing the customized generic"),
kapps_call_command:audio_macro([{'prompt', <<"vm-person_not_available">>}], Call);
play_greeting(#mailbox{unavailable_media_id='undefined'
,mailbox_number=Mailbox
}, Call) ->
lager:debug("mailbox has no greeting, playing the generic"),
kapps_call_command:audio_macro([{'prompt', <<"vm-person">>}
,{'say', Mailbox}
,{'prompt', <<"vm-not_available">>}
], Call);
play_greeting(#mailbox{unavailable_media_id=MediaId}, Call) ->
Corrected = kz_media_util:media_path(MediaId, kapps_call:account_id(Call)),
lager:info("mailbox has a greeting: '~s', corrected to '~s'", [MediaId, Corrected]),
kapps_call_command:play(Corrected, Call).
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec play_instructions(mailbox(), kapps_call:call()) -> kz_term:ne_binary() | 'ok'.
play_instructions(#mailbox{skip_instructions='true'}, _) -> 'ok';
play_instructions(#mailbox{skip_instructions='false'}, Call) ->
kapps_call_command:prompt(<<"vm-record_message">>, Call).
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec record_voicemail(kz_term:ne_binary(), mailbox(), kapps_call:call()) -> 'ok'.
record_voicemail(AttachmentName, #mailbox{max_message_length=MaxMessageLength
,media_extension=Ext
}=Box, Call) ->
Tone = kz_json:from_list([{<<"Frequencies">>, [<<"440">>]}
,{<<"Duration-ON">>, <<"500">>}
,{<<"Duration-OFF">>, <<"100">>}
]),
kapps_call_command:tones([Tone], Call),
lager:info("composing new voicemail to ~s", [AttachmentName]),
Routines = [{fun kapps_call:set_message_left/2, 'true'}],
case kapps_call_command:b_record(AttachmentName, ?ANY_DIGIT, kz_term:to_binary(MaxMessageLength), Call) of
{'ok', Msg} ->
Length = kz_json:get_integer_value(<<"Length">>, Msg, 0),
case kz_call_event:hangup_cause(Msg) =:= 'undefined'
andalso review_recording(AttachmentName, 'true', Box, Call)
of
'false' ->
_ = cf_exe:update_call(Call, Routines),
new_message(AttachmentName, Length, Box, Call);
{'ok', 'record'} ->
record_voicemail(tmp_file(Ext), Box, Call);
{'ok', _Selection} ->
_ = cf_exe:update_call(Call, Routines),
cf_util:start_task(fun new_message/4, [AttachmentName, Length, Box], Call),
_ = kapps_call_command:prompt(<<"vm-saved">>, Call),
_ = kapps_call_command:prompt(<<"vm-thank_you">>, Call),
'ok';
{'branch', Flow} ->
_ = cf_exe:update_call(Call, Routines),
_ = new_message(AttachmentName, Length, Box, Call),
_ = kapps_call_command:prompt(<<"vm-saved">>, Call),
{'branch', Flow}
end;
{'error', _R} ->
lager:info("error while attempting to record a new message: ~p", [_R])
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec setup_mailbox(mailbox(), kapps_call:call()) ->
mailbox() |
{'error', 'channel_hungup'}.
setup_mailbox(#mailbox{media_extension=Ext}=Box, Call) ->
lager:debug("starting voicemail configuration wizard"),
{'ok', _} = kapps_call_command:b_prompt(<<"vm-setup_intro">>, Call),
lager:info("prompting caller to set a pin"),
case change_pin(Box, Call) of
#mailbox{} ->
{'ok', _} = kapps_call_command:b_prompt(<<"vm-setup_rec_greeting">>, Call),
lager:info("prompting caller to record an unavailable greeting"),
#mailbox{}=Box1 = record_unavailable_greeting(tmp_file(Ext), Box, Call),
'ok' = update_doc(<<"is_setup">>, 'true', Box1, Call),
lager:info("voicemail configuration wizard is complete"),
{'ok', _} = kapps_call_command:b_prompt(<<"vm-setup_complete">>, Call),
Box1#mailbox{is_setup='true'};
{'error', 'max_retry'} ->
lager:debug("hanging up channel after several empty or invalid pins"),
_ = kapps_call_command:b_prompt(<<"vm-goodbye">>, Call),
{'error', 'channel_hungup'}
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec main_menu(mailbox(), kapps_call:call()) ->
'ok' | {'error', 'channel_hungup'}.
main_menu(#mailbox{is_setup='false'}=Box, Call) ->
try setup_mailbox(Box, Call) of
#mailbox{}=Box1 -> main_menu(Box1, Call, 1);
{'error', 'channel_hungup'} = Err -> Err
catch
'error':{'badmatch',{'error','channel_hungup'}} ->
lager:debug("channel has hungup while setting up mailbox"),
{'error', 'channel_hungup'};
_E:_R ->
lager:debug("failed to setup mailbox: ~s: ~p", [_E, _R])
end;
main_menu(Box, Call) -> main_menu(Box, Call, 1).
-spec main_menu(mailbox(), kapps_call:call(), non_neg_integer()) ->
'ok' | {'error', 'channel_hungup'}.
main_menu(Box, Call, Loop) when Loop > 4 ->
%% If there have been too may loops with no action from the caller this
%% is likely a abandoned channel, terminate
lager:info("entered main menu with too many invalid entries"),
_ = kapps_call_command:b_prompt(<<"vm-goodbye">>, Call),
send_mwi_update(Box);
main_menu(#mailbox{keys=#keys{hear_new=HearNew
,hear_saved=HearSaved
,exit=Exit
}
,interdigit_timeout=Interdigit
,not_configurable='true'
,mailbox_id=BoxId
}=Box, Call, Loop) ->
lager:debug("playing mailbox main menu"),
_ = kapps_call_command:b_flush(Call),
Messages = kvm_messages:get(kapps_call:account_id(Call), BoxId),
New = kzd_box_message:count_folder(Messages, ?VM_FOLDER_NEW),
Saved = kzd_box_message:count_folder(Messages, ?VM_FOLDER_SAVED),
lager:debug("mailbox has ~p new and ~p saved messages", [New, Saved]),
NoopId = kapps_call_command:audio_macro(message_count_prompts(New, Saved)
++ [{'prompt', <<"vm-main_menu_not_configurable">>}]
,Call
),
case kapps_call_command:collect_digits(?KEY_LENGTH
,kapps_call_command:default_collect_timeout()
,Interdigit
,NoopId
,Call
)
of
{'error', _} ->
lager:info("error during mailbox main menu"),
send_mwi_update(Box);
{'ok', Exit} ->
lager:info("user choose to exit voicemail menu"),
send_mwi_update(Box);
{'ok', HearNew} ->
lager:info("playing all messages in folder: ~s", [?VM_FOLDER_NEW]),
Folder = kzd_box_message:filter_folder(Messages, ?VM_FOLDER_NEW),
case play_messages(Folder, New, Box, Call) of
'ok' -> send_mwi_update(Box);
_Else -> main_menu(Box, Call)
end;
{'ok', HearSaved} ->
lager:info("playing all messages in folder: ~s", [?VM_FOLDER_SAVED]),
Folder = kzd_box_message:filter_folder(Messages, ?VM_FOLDER_SAVED),
case play_messages(Folder, Saved, Box, Call) of
'ok' -> send_mwi_update(Box);
_Else -> main_menu(Box, Call)
end;
_ ->
main_menu(Box, Call, Loop + 1)
end;
main_menu(#mailbox{keys=#keys{hear_new=HearNew
,hear_saved=HearSaved
,configure=Configure
,exit=Exit
}
,interdigit_timeout=Interdigit
,not_configurable='false'
,mailbox_id=BoxId
}=Box, Call, Loop) ->
lager:debug("playing mailbox main menu"),
_ = kapps_call_command:b_flush(Call),
Messages = kvm_messages:get(kapps_call:account_id(Call), BoxId),
New = kzd_box_message:count_folder(Messages, ?VM_FOLDER_NEW),
Saved = kzd_box_message:count_folder(Messages, ?VM_FOLDER_SAVED),
lager:debug("mailbox has ~p new and ~p saved messages", [New, Saved]),
NoopId = kapps_call_command:audio_macro(message_count_prompts(New, Saved)
++ [{'prompt', <<"vm-main_menu">>}]
,Call),
case kapps_call_command:collect_digits(?KEY_LENGTH
,kapps_call_command:default_collect_timeout()
,Interdigit
,NoopId
,Call
)
of
{'error', _} ->
lager:info("error during mailbox main menu"),
send_mwi_update(Box);
{'ok', Exit} ->
lager:info("user choose to exit voicemail menu"),
send_mwi_update(Box);
{'ok', HearNew} ->
lager:info("playing all messages in folder: ~s", [?VM_FOLDER_NEW]),
Folder = kzd_box_message:filter_folder(Messages, ?VM_FOLDER_NEW),
case play_messages(Folder, New, Box, Call) of
'ok' -> send_mwi_update(Box);
_Else -> main_menu(Box, Call)
end;
{'ok', HearSaved} ->
lager:info("playing all messages in folder: ~s", [?VM_FOLDER_SAVED]),
Folder = kzd_box_message:filter_folder(Messages, ?VM_FOLDER_SAVED),
case play_messages(Folder, Saved, Box, Call) of
'ok' -> send_mwi_update(Box);
_Else -> main_menu(Box, Call)
end;
{'ok', Configure} ->
lager:info("caller chose to change their mailbox configuration"),
case config_menu(Box, Call) of
'ok' -> 'ok';
{'error', 'channel_hungup'}=E ->
lager:debug("channel has hungup, done trying to setup mailbox"),
E;
#mailbox{}=Box1 -> main_menu(Box1, Call)
end;
_ ->
main_menu(Box, Call, Loop + 1)
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec message_count_prompts(integer(), integer()) -> kz_term:proplist().
message_count_prompts(0, 0) ->
[{'prompt', <<"vm-no_messages">>}];
message_count_prompts(1, 0) ->
[{'prompt', <<"vm-you_have">>}
,{'say', <<"1">>, ?VM_KEY_MESSAGES}
,{'prompt', <<"vm-new_message">>}
];
message_count_prompts(0, 1) ->
[{'prompt', <<"vm-you_have">>}
,{'say', <<"1">>, ?VM_KEY_MESSAGES}
,{'prompt', <<"vm-saved_message">>}
];
message_count_prompts(1, 1) ->
[{'prompt', <<"vm-you_have">>}
,{'say', <<"1">>, ?VM_KEY_MESSAGES}
,{'prompt', <<"vm-new_and">>}
,{'say', <<"1">>, ?VM_KEY_MESSAGES}
,{'prompt', <<"vm-saved_message">>}
];
message_count_prompts(New, 0) ->
[{'prompt', <<"vm-you_have">>}
,{'say', kz_term:to_binary(New), ?VM_KEY_MESSAGES}
,{'prompt', <<"vm-new_messages">>}
];
message_count_prompts(New, 1) ->
[{'prompt', <<"vm-you_have">>}
,{'say', kz_term:to_binary(New), ?VM_KEY_MESSAGES}
,{'prompt', <<"vm-new_and">>}
,{'say', <<"1">>, ?VM_KEY_MESSAGES}
,{'prompt', <<"vm-saved_message">>}
];
message_count_prompts(0, Saved) ->
[{'prompt', <<"vm-you_have">>}
,{'say', kz_term:to_binary(Saved), ?VM_KEY_MESSAGES}
,{'prompt', <<"vm-saved_messages">>}
];
message_count_prompts(1, Saved) ->
[{'prompt', <<"vm-you_have">>}
,{'say', <<"1">>, ?VM_KEY_MESSAGES}
,{'prompt', <<"vm-new_and">>}
,{'say', kz_term:to_binary(Saved), ?VM_KEY_MESSAGES}
,{'prompt', <<"vm-saved_messages">>}
];
message_count_prompts(New, Saved) ->
[{'prompt', <<"vm-you_have">>}
,{'say', kz_term:to_binary(New), ?VM_KEY_MESSAGES}
,{'prompt', <<"vm-new_and">>}
,{'say', kz_term:to_binary(Saved), ?VM_KEY_MESSAGES}
,{'prompt', <<"vm-saved_messages">>}
].
%%------------------------------------------------------------------------------
%% @doc Returns the message audio prompt
%% @end
%%------------------------------------------------------------------------------
-spec message_prompt(kz_json:objects(), binary(), non_neg_integer(), mailbox()) ->
kapps_call_command:audio_macro_prompts().
message_prompt([H|_]=Messages, Message, Count, #mailbox{timezone=Timezone
,skip_envelope='false'
}) ->
[{'prompt', <<"vm-message_number">>}
,{'say', kz_term:to_binary(Count - length(Messages) + 1), <<"number">>}
,{'play', Message}
,{'prompt', <<"vm-received">>}
,{'say', get_unix_epoch(kz_json:get_integer_value(<<"timestamp">>, H), Timezone), <<"current_date_time">>}
,{'prompt', <<"vm-message_menu">>}
];
message_prompt(Messages, Message, Count, #mailbox{skip_envelope='true'}) ->
lager:debug("mailbox is set to skip playing message envelope"),
[{'prompt', <<"vm-message_number">>}
,{'say', kz_term:to_binary(Count - length(Messages) + 1), <<"number">>}
,{'play', Message}
,{'prompt', <<"vm-message_menu">>}
].
%%------------------------------------------------------------------------------
%% @doc Plays back a message then the menu, and continues to loop over the
%% menu util
%% @end
%%------------------------------------------------------------------------------
-spec play_messages(kz_json:objects(), non_neg_integer(), mailbox(), kapps_call:call()) ->
'ok' | 'complete'.
play_messages(Messages, Count, Box, Call) ->
play_messages(Messages, [], Count, Box, Call).
-spec play_messages(kz_json:objects(), kz_json:objects(), non_neg_integer(), mailbox(), kapps_call:call()) ->
'ok' | 'complete'.
play_messages([H|T]=Messages, PrevMessages, Count, Box, Call) ->
AccountId = kapps_call:account_id(Call),
Message = kvm_message:media_url(AccountId, H),
lager:info("playing mailbox message ~p (~s)", [Count, Message]),
Prompt = message_prompt(Messages, Message, Count, Box),
case message_menu(Prompt, Box, Call) of
{'ok', 'keep'} ->
lager:info("caller chose to save the message"),
_ = kapps_call_command:b_prompt(<<"vm-saved">>, Call),
{_, NMessage} = kvm_message:set_folder(?VM_FOLDER_SAVED, H, AccountId),
play_messages(T, [NMessage|PrevMessages], Count, Box, Call);
{'ok', 'prev'} ->
lager:info("caller chose to listen to previous message"),
play_prev_message(Messages, PrevMessages, Count, Box, Call);
{'ok', 'next'} ->
lager:info("caller chose to listen to next message"),
play_next_message(Messages, PrevMessages, Count, Box, Call);
{'ok', 'delete'} ->
lager:info("caller chose to delete the message"),
_ = kapps_call_command:b_prompt(<<"vm-deleted">>, Call),
_ = kvm_message:set_folder({?VM_FOLDER_DELETED, 'false'}, H, AccountId),
play_messages(T, PrevMessages, Count, Box, Call);
{'ok', 'return'} ->
lager:info("caller chose to return to the main menu"),
_ = kapps_call_command:b_prompt(<<"vm-saved">>, Call),
_ = kvm_message:set_folder(?VM_FOLDER_SAVED, H, AccountId),
'complete';
{'ok', 'replay'} ->
lager:info("caller chose to replay"),
play_messages(Messages, PrevMessages, Count, Box, Call);
{'ok', 'forward'} ->
lager:info("caller chose to forward the message"),
forward_message(H, Box, Call),
{_, NMessage} = kvm_message:set_folder(?VM_FOLDER_SAVED, H, AccountId),
_ = kapps_call_command:prompt(<<"vm-saved">>, Call),
play_messages(T, [NMessage|PrevMessages], Count, Box, Call);
{'error', _} ->
lager:info("error during message playback")
end;
play_messages([], _, _, _, _) ->
lager:info("all messages in folder played to caller"),
'complete'.
-spec play_next_message(kz_json:objects(), kz_json:objects(), non_neg_integer(), mailbox(), kapps_call:call()) ->
'ok' | 'complete'.
play_next_message([_] = Messages, PrevMessages, Count, Box, Call) ->
play_messages(Messages, PrevMessages, Count, Box, Call);
play_next_message([H|T], PrevMessages, Count, Box, Call) ->
play_messages(T, [H|PrevMessages], Count, Box, Call).
-spec play_prev_message(kz_json:objects(), kz_json:objects(), non_neg_integer(), mailbox(), kapps_call:call()) ->
'ok' | 'complete'.
play_prev_message(Messages, [] = PrevMessages, Count, Box, Call) ->
play_messages(Messages, PrevMessages, Count, Box, Call);
play_prev_message(Messages, [H|T], Count, Box, Call) ->
play_messages([H|Messages], T, Count, Box, Call).
%%------------------------------------------------------------------------------
%% @doc Main function for forwarding a message to another vmbox of this account
%% @end
%%------------------------------------------------------------------------------
-spec forward_message(kz_json:object(), mailbox(), kapps_call:call()) -> 'ok'.
forward_message(Message, #mailbox{mailbox_id=SrcBoxId} = SrcBox, Call) ->
lager:info("enter destination mailbox number"),
_ = kapps_call_command:b_flush(Call),
PossibleBox = find_destination_mailbox(#mailbox{}, Call, SrcBoxId, 1),
forward_message(Message, SrcBox, PossibleBox, Call).
-spec forward_message(kz_json:object(), mailbox(), mailbox(), kapps_call:call()) -> 'ok'.
forward_message(_Message, _SrcBox, #mailbox{exists='false'}, _Call) ->
lager:info("unable to find destination mailbox, returning to message menu...");
forward_message(Message, #mailbox{mailbox_id = SrcBoxId
,forward_type = ForwardType
}, DestBox, Call) ->
case ForwardType =:= <<"prepend_forward">>
andalso forward_message_menu(DestBox, Call)
of
'false' ->
forward_message('undefined', 0, Message, SrcBoxId, DestBox, Call);
{'ok', 'return'} -> 'ok';
{'ok', 'append'} ->
compose_forward_message(Message, SrcBoxId, DestBox, Call);
{'ok', 'forward'} ->
forward_message('undefined', 0, Message, SrcBoxId, DestBox, Call);
{'error', _R} ->
lager:info("error during forward message playback: ~p", [_R])
end.
-spec forward_message_menu(mailbox(), kapps_call:call()) ->
{'error', 'channel_hungup' | 'channel_unbridge' | kz_json:object()} |
{'ok', 'append' | 'forward' | 'return'}.
forward_message_menu(#mailbox{interdigit_timeout=Interdigit}=DestBox, Call) ->
lager:info("playing forward message menu"),
Prompt = [{'prompt', <<"vm-message_forwarding">>}],
NoopId = kapps_call_command:audio_macro(Prompt, Call),
case kapps_call_command:collect_digits(?KEY_LENGTH
,kapps_call_command:default_collect_timeout()
,Interdigit
,NoopId
,Call
)
of
{'ok', <<"1">>} -> {'ok', 'append'};
{'ok', <<"2">>} -> {'ok', 'forward'};
{'ok', <<"0">>} -> {'ok', 'return'};
{'error', _}=Error -> Error;
_ -> forward_message_menu(DestBox, Call)
end.
-spec compose_forward_message(kz_json:object(), kz_term:ne_binary(), mailbox(), kapps_call:call()) -> 'ok'.
compose_forward_message(Message, SrcBoxId, #mailbox{media_extension=Ext}=DestBox, Call) ->
lager:debug("playing forwarding instructions to caller"),
_ = play_instructions(DestBox, Call),
_NoopId = kapps_call_command:noop(Call),
%% timeout after 5 min for safety, so this process cant hang around forever
case kapps_call_command:wait_for_application_or_dtmf(<<"noop">>, 300000) of
{'ok', _} ->
lager:info("played forwarding instructions to caller, recording new message"),
record_forward(tmp_file(Ext), Message, SrcBoxId, DestBox, Call);
{'dtmf', _Digits} ->
_ = kapps_call_command:b_flush(Call),
lager:info("recording forwarding message"),
record_forward(tmp_file(Ext), Message, SrcBoxId, DestBox, Call);
{'error', _R} ->
lager:info("error while playing voicemail greeting: ~p", [_R])
end.
-spec record_forward(kz_term:ne_binary(), kz_json:object(), kz_term:ne_binary(), mailbox(), kapps_call:call()) -> 'ok'.
record_forward(AttachmentName, Message, SrcBoxId, #mailbox{media_extension=Ext
,max_message_length=MaxMessageLength
}=DestBox, Call) ->
Tone = kz_json:from_list([{<<"Frequencies">>, [<<"440">>]}
,{<<"Duration-ON">>, <<"500">>}
,{<<"Duration-OFF">>, <<"100">>}
]),
lager:info("composing new voicemail forward to ~s", [AttachmentName]),
kapps_call_command:tones([Tone], Call),
case kapps_call_command:b_record(AttachmentName, ?ANY_DIGIT, kz_term:to_binary(MaxMessageLength), Call) of
{'ok', Msg} ->
Length = kz_json:get_integer_value(<<"Length">>, Msg, 0),
case kz_call_event:hangup_cause(Msg) =:= 'undefined'
andalso review_recording(AttachmentName, 'false', DestBox, Call)
of
'false' ->
forward_message(AttachmentName, Length, Message, SrcBoxId, DestBox, Call);
{'ok', 'record'} ->
record_forward(tmp_file(Ext), Message, SrcBoxId, DestBox, Call);
{'ok', _Selection} ->
cf_util:start_task(fun forward_message/6
,[AttachmentName, Length, Message, SrcBoxId, DestBox]
, Call
)
end;
{'error', _R} ->
lager:info("error while attempting to record a forward message: ~p", [_R])
end.
-spec forward_message(kz_term:api_ne_binary(), non_neg_integer(), kz_json:object(), kz_term:ne_binary(), mailbox(), kapps_call:call()) -> 'ok'.
forward_message(AttachmentName, Length, Message, SrcBoxId, #mailbox{mailbox_number=BoxNum
,mailbox_id=BoxId
,timezone=Timezone
,owner_id=OwnerId
,transcribe_voicemail=MaybeTranscribe
,after_notify_action=Action
,media_extension=Extension
}=DestBox, Call) ->
NewMsgProps = props:filter_undefined(
[{<<"Box-Id">>, BoxId}
,{<<"Owner-Id">>, OwnerId}
,{<<"Length">>, Length + kz_json:get_integer_value(<<"length">>, Message, 0)}
,{<<"Transcribe-Voicemail">>, MaybeTranscribe}
,{<<"After-Notify-Action">>, Action}
,{<<"Attachment-Name">>, AttachmentName}
,{<<"Box-Num">>, BoxNum}
,{<<"Timezone">>, Timezone}
,{<<"Description">>, <<"forwarded voicemail message with media">>}
,{<<"Media-Extension">>, Extension}
]
),
case kvm_message:forward_message(Call, Message, SrcBoxId, NewMsgProps) of
{'ok', _NewCall} -> send_mwi_update(DestBox);
{'error', _, _Msg} ->
lager:warning("failed to save forwarded voice mail message recorded media : ~p", [_Msg])
end.
%%------------------------------------------------------------------------------
%% @doc Loops over the message menu after the first play back util the
%% user provides a valid option
%% @end
%%------------------------------------------------------------------------------
-type message_menu_returns() :: {'ok', 'keep' | 'delete' | 'return' | 'replay' | 'prev' | 'next' | 'forward'}.
-spec message_menu(mailbox(), kapps_call:call()) ->
{'error', 'channel_hungup' | 'channel_unbridge' | kz_json:object()} |
message_menu_returns().
message_menu(Box, Call) ->
message_menu([{'prompt', <<"vm-message_menu">>}], Box, Call).
-spec message_menu(kapps_call_command:audio_macro_prompts(), mailbox(), kapps_call:call()) ->
{'error', 'channel_hungup' | 'channel_unbridge' | kz_json:object()} |
message_menu_returns().
message_menu(Prompt, #mailbox{keys=#keys{replay=Replay
,keep=Keep
,forward=Forward
,delete=Delete
,prev=Prev
,next=Next
,return_main=ReturnMain
}
,interdigit_timeout=Interdigit
}=Box, Call) ->
lager:info("playing message menu"),
NoopId = kapps_call_command:audio_macro(Prompt, Call),
case kapps_call_command:collect_digits(?KEY_LENGTH
,kapps_call_command:default_collect_timeout()
,Interdigit
,NoopId
,Call
)
of
{'ok', Keep} -> {'ok', 'keep'};
{'ok', Forward} -> {'ok', 'forward'};
{'ok', Delete} -> {'ok', 'delete'};
{'ok', ReturnMain} -> {'ok', 'return'};
{'ok', Replay} -> {'ok', 'replay'};
{'ok', Prev} -> {'ok', 'prev'};
{'ok', Next} -> {'ok', 'next'};
{'error', _}=E -> E;
_ ->
_ = kapps_call_command:b_prompt(<<"menu-invalid_entry">>, Call),
message_menu(Box, Call)
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec config_menu(mailbox(), kapps_call:call()) ->
'ok' | mailbox() |
{'error', 'channel_hungup'}.
config_menu(Box, Call) ->
config_menu(Box, Call, 1).
-spec config_menu(mailbox(), kapps_call:call(), pos_integer()) ->
'ok' | mailbox() |
{'error', 'channel_hungup'}.
config_menu(#mailbox{interdigit_timeout=Interdigit}=Box
,Call
,Loop
) when Loop < 4 ->
lager:info("playing mailbox configuration menu"),
{'ok', _} = kapps_call_command:b_flush(Call),
NoopId = kapps_call_command:prompt(<<"vm-settings_menu">>, Call),
case kapps_call_command:collect_digits(?KEY_LENGTH
,kapps_call_command:default_collect_timeout()
,Interdigit
,NoopId
,Call
)
of
{'ok', Selection} ->
handle_config_selection(Box, Call, Loop, Selection);
{'error', _E} ->
lager:info("failed to collect config menu selection: ~p", [_E])
end.
-spec handle_config_selection(mailbox(), kapps_call:call(), pos_integer(), binary()) ->
'ok' | mailbox() |
{'error', 'channel_hungup'}.
handle_config_selection(#mailbox{keys=#keys{rec_unavailable=Selection}
,media_extension=Ext
}=Box
,Call
,_Loop
,Selection
) ->
lager:info("caller chose to record their unavailable greeting"),
case record_unavailable_greeting(tmp_file(Ext), Box, Call) of
'ok' -> 'ok';
Else -> config_menu(Else, Call)
end;
handle_config_selection(#mailbox{keys=#keys{rec_name=Selection}
,media_extension=Ext
}=Box
,Call
,_Loop
,Selection
) ->
lager:info("caller chose to record their name"),
case record_name(tmp_file(Ext), Box, Call) of
'ok' -> 'ok';
Else -> config_menu(Else, Call)
end;
handle_config_selection(#mailbox{keys=#keys{set_pin=Selection}}=Box
,Call
,_Loop
,Selection
) ->
lager:info("caller chose to change their pin"),
case change_pin(Box, Call) of
{'error', 'channel_hungup'}=E ->
lager:debug("channel has hungup, done trying to setup mailbox"),
E;
{'error', _E} ->
lager:debug("changing pin failed: ~p", [_E]),
config_menu(Box, Call);
#mailbox{}=Box1 ->
config_menu(Box1, Call)
end;
handle_config_selection(#mailbox{keys=#keys{rec_temporary_unavailable=Selection}
,media_extension=Ext
}=Box
,Call
,_Loop
,Selection
) ->
lager:info("caller chose to record their temporary unavailable greeting"),
case record_temporary_unavailable_greeting(tmp_file(Ext), Box, Call) of
'ok' -> 'ok';
Box1 -> config_menu(Box1, Call)
end;
handle_config_selection(#mailbox{keys=#keys{del_temporary_unavailable=Selection}}=Box
,Call
,_Loop
,Selection
) ->
lager:info("caller chose to delete their temporary unavailable greeting"),
delete_temporary_unavailable_greeting(Box, Call);
handle_config_selection(#mailbox{keys=#keys{return_main=Selection}}=Box
,_Call
,_Loop
,Selection
) ->
lager:info("caller chose to return to the main menu"),
Box;
%% Bulk delete -> delete all voicemails
%% Reset -> delete all voicemails, greetings, name, and reset pin
handle_config_selection(#mailbox{}=Box
,Call
,Loop
,_Selection
) ->
lager:info("undefined config menu option '~s'", [_Selection]),
config_menu(Box, Call, Loop + 1).
%%------------------------------------------------------------------------------
%% @doc Recording the temporary greeting to override the common greeting
%% @end
%%------------------------------------------------------------------------------
-spec record_temporary_unavailable_greeting(kz_term:ne_binary(), mailbox(), kapps_call:call()) ->
'ok' | mailbox().
record_temporary_unavailable_greeting(AttachmentName
,#mailbox{temporary_unavailable_media_id='undefined'}=Box
,Call
) ->
lager:info("no temporary greetings was recorded before so new media document should be created"),
MediaId = recording_media_doc(<<"temporary unavailable greeting">>, Box, Call),
overwrite_temporary_unavailable_greeting(AttachmentName
,Box#mailbox{temporary_unavailable_media_id=MediaId}
,Call
,'new'
);
record_temporary_unavailable_greeting(AttachmentName, Box, Call) ->
lager:info("record new temporary greetings use existing media document"),
overwrite_temporary_unavailable_greeting(AttachmentName, Box, Call, 'update').
%%------------------------------------------------------------------------------
%% @doc Overwrites current media document of the temporary greeting
%% by a new recorded version.
%% @end
%%------------------------------------------------------------------------------
-spec overwrite_temporary_unavailable_greeting(kz_term:ne_binary(), mailbox(), kapps_call:call(), 'new' | 'update') ->
'ok' | mailbox().
overwrite_temporary_unavailable_greeting(AttachmentName
,#mailbox{temporary_unavailable_media_id=MediaId
,media_extension=Ext
}=Box
,Call
,UpdateOrNew
) ->
lager:info("overwriting temporary unavailable greeting as ~s", [AttachmentName]),
Tone = kz_json:from_list([{<<"Frequencies">>, [<<"440">>]}
,{<<"Duration-ON">>, <<"500">>}
,{<<"Duration-OFF">>, <<"100">>}
]),
_NoopId = kapps_call_command:audio_macro([{'prompt', <<"vm-record_temp_greeting">>}
,{'tones', [Tone]}
]
,Call
),
case kapps_call_command:b_record(AttachmentName, Call) of
{'ok', Msg} ->
case review_recording(AttachmentName, 'false', Box, Call) of
{'ok', 'record'} ->
lager:info("selected item: record new temporary greetings"),
record_temporary_unavailable_greeting(tmp_file(Ext), Box, Call);
{'ok', 'save'} ->
lager:info("selected item: store recorded temporary greetings"),
Length = kz_json:get_integer_value(<<"Length">>, Msg, 0),
_ = store_recording(AttachmentName, Length, MediaId, Box, Call),
'ok' = update_doc([<<"media">>, <<"temporary_unavailable">>], MediaId, Box, Call),
maybe_update_recording_media_doc(AttachmentName, Box, Call, MediaId, UpdateOrNew),
_ = kapps_call_command:b_prompt(<<"vm-saved">>, Call),
Box;
{'ok', 'no_selection'} ->
lager:info("selected item: no selection"),
_ = kapps_call_command:b_prompt(<<"vm-deleted">>, Call),
'ok';
{'branch', _}=B -> B
end;
{'error', _R} ->
lager:info("error while attempting to record temporary unavailable recording: ~p", [_R])
end.
%%------------------------------------------------------------------------------
%% @doc Deletes current temporary greeting.
%% @end
%%------------------------------------------------------------------------------
-spec delete_temporary_unavailable_greeting(mailbox(), kapps_call:call()) -> mailbox().
delete_temporary_unavailable_greeting(#mailbox{temporary_unavailable_media_id='undefined'}=Box, _Call) ->
Box;
delete_temporary_unavailable_greeting(Box, Call) ->
'ok' = update_doc([<<"media">>, <<"temporary_unavailable">>], 'null', Box, Call),
_ = kapps_call_command:b_prompt(<<"vm-saved">>, Call),
Box#mailbox{temporary_unavailable_media_id='undefined'}.
-spec record_unavailable_greeting(kz_term:ne_binary(), mailbox(), kapps_call:call()) ->
'ok' | mailbox().
record_unavailable_greeting(AttachmentName, #mailbox{unavailable_media_id='undefined'}=Box, Call) ->
MediaId = recording_media_doc(<<"unavailable greeting">>, Box, Call),
overwrite_unavailable_greeting(AttachmentName, Box#mailbox{unavailable_media_id=MediaId}, Call, MediaId, 'new');
record_unavailable_greeting(AttachmentName, #mailbox{unavailable_media_id=MediaId}=Box, Call) ->
case kz_datamgr:open_cache_doc(kapps_call:account_db(Call), MediaId) of
{'ok', JObj} -> check_media_source(AttachmentName, Box, Call, JObj);
_ -> overwrite_unavailable_greeting(AttachmentName, Box, Call, MediaId, 'update')
end.
-spec check_media_source(kz_term:ne_binary(), mailbox(), kapps_call:call(), kz_json:object()) ->
'ok' | mailbox().
check_media_source(AttachmentName, Box, Call, JObj) ->
case kz_json:get_ne_binary_value(<<"media_source">>, JObj) of
<<"upload">> ->
lager:debug("the voicemail greeting media is a web upload, let's not touch it,"
++ " it may be in use in some other mailbox. We create new media document."
),
record_unavailable_greeting(AttachmentName, Box#mailbox{unavailable_media_id='undefined'}, Call);
_ ->
overwrite_unavailable_greeting(AttachmentName, Box, Call, JObj, 'update')
end.
-spec overwrite_unavailable_greeting(kz_term:ne_binary(), mailbox(), kapps_call:call(), kz_json:object() | kz_term:ne_binary(), 'new' | 'update') ->
'ok' | mailbox().
overwrite_unavailable_greeting(AttachmentName, #mailbox{unavailable_media_id=MediaId
,media_extension=Ext
}=Box, Call, JObjOrID, UpdateOrNew) ->
lager:info("overwriting unavailable greeting as ~s", [AttachmentName]),
Tone = kz_json:from_list([{<<"Frequencies">>, [<<"440">>]}
,{<<"Duration-ON">>, <<"500">>}
,{<<"Duration-OFF">>, <<"100">>}
]),
_NoopId = kapps_call_command:audio_macro([{'prompt', <<"vm-record_greeting">>}
,{'tones', [Tone]}
]
,Call
),
case kapps_call_command:b_record(AttachmentName, Call) of
{'ok', Msg} ->
case review_recording(AttachmentName, 'false', Box, Call) of
{'ok', 'record'} ->
record_unavailable_greeting(tmp_file(Ext), Box, Call);
{'ok', 'save'} ->
Length = kz_json:get_integer_value(<<"Length">>, Msg, 0),
_ = store_recording(AttachmentName, Length, MediaId, Box, Call),
'ok' = update_doc([<<"media">>, <<"unavailable">>], MediaId, Box, Call),
maybe_update_recording_media_doc(AttachmentName, Box, Call, JObjOrID, UpdateOrNew),
_ = kapps_call_command:b_prompt(<<"vm-saved">>, Call),
Box;
{'ok', 'no_selection'} ->
_ = kapps_call_command:b_prompt(<<"vm-deleted">>, Call),
'ok';
{'branch', _}=B -> B
end;
{'error', _R} ->
lager:info("error while attempting to record unavailable recording: ~p", [_R])
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec record_name(kz_term:ne_binary(), mailbox(), kapps_call:call()) ->
'ok' | mailbox().
record_name(AttachmentName, #mailbox{owner_id='undefined'
,name_media_id='undefined'
,mailbox_id=BoxId
}=Box, Call) ->
lager:info("no recorded name media id nor owner id"),
MediaId = recording_media_doc(<<"users name">>, Box, Call),
lager:info("created recorded name media doc: ~s , saving recorded name id into mailbox", [MediaId]),
record_name(AttachmentName, Box#mailbox{name_media_id=MediaId}, Call, BoxId, 'new');
record_name(AttachmentName, #mailbox{owner_id='undefined'
,mailbox_id=BoxId
}=Box, Call) ->
lager:info("no owner_id set on mailbox, saving recorded name id into mailbox"),
record_name(AttachmentName, Box, Call, BoxId, 'update');
record_name(AttachmentName, #mailbox{owner_id=OwnerId
,name_media_id='undefined'
}=Box, Call) ->
lager:info("no recorded name media id for owner"),
MediaId = recording_media_doc(<<"users name">>, Box, Call),
lager:info("created recorded name media doc: ~s", [MediaId]),
record_name(AttachmentName, Box#mailbox{name_media_id=MediaId}, Call, OwnerId, 'new');
record_name(AttachmentName, #mailbox{owner_id=OwnerId}=Box, Call) ->
lager:info("owner_id (~s) set on mailbox, saving into owner's doc", [OwnerId]),
record_name(AttachmentName, Box, Call, OwnerId, 'update').
-spec record_name(kz_term:ne_binary(), mailbox(), kapps_call:call(), kz_term:ne_binary(), 'new' | 'update') ->
'ok' | mailbox().
record_name(AttachmentName, #mailbox{name_media_id=MediaId
,media_extension=Ext
}=Box, Call, DocId, UpdateOrNew) ->
lager:info("recording name as ~s in ~s", [AttachmentName, MediaId]),
Tone = kz_json:from_list([{<<"Frequencies">>, [<<"440">>]}
,{<<"Duration-ON">>, <<"500">>}
,{<<"Duration-OFF">>, <<"100">>}
]),
_NoopId = kapps_call_command:audio_macro([{'prompt', <<"vm-record_name">>}
,{'tones', [Tone]}
], Call),
case kapps_call_command:b_record(AttachmentName, Call) of
{'ok', Msg} ->
case review_recording(AttachmentName, 'false', Box, Call) of
{'ok', 'record'} ->
record_name(tmp_file(Ext), Box, Call);
{'ok', 'save'} ->
Length = kz_json:get_integer_value(<<"Length">>, Msg, 0),
_ = store_recording(AttachmentName, Length, MediaId, Box, Call),
'ok' = update_doc(?RECORDED_NAME_KEY, MediaId, DocId, Call),
maybe_update_recording_media_doc(AttachmentName, Box, Call, MediaId, UpdateOrNew),
_ = kapps_call_command:b_prompt(<<"vm-saved">>, Call),
Box;
{'ok', 'no_selection'} ->
_ = kapps_call_command:b_prompt(<<"vm-deleted">>, Call),
'ok';
{'branch', _}=B -> B
end;
{'error', _R} ->
lager:info("error while attempting to record recording name: ~p", [_R])
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec change_pin(mailbox(), kapps_call:call()) ->
mailbox() | {'error', any()}.
change_pin(Box, Call) ->
change_pin(Box, Call, 1).
-spec change_pin(mailbox(), kapps_call:call(), non_neg_integer()) ->
mailbox() | {'error', any()}.
change_pin(#mailbox{mailbox_id=Id
,interdigit_timeout=Interdigit
}=Box
,Call
,Loop
) ->
lager:info("requesting new mailbox pin number (loop ~p)", [Loop]),
try
{'ok', Pin} = get_new_pin(Interdigit, Call),
lager:info("collected first pin"),
{'ok', Pin} = confirm_new_pin(Interdigit, Call),
lager:info("collected second pin"),
Pin =:= <<>>
andalso throw('pin_empty'),
lager:info("entered pin is not empty"),
AccountDb = kapps_call:account_db(Call),
{'ok', JObj} = kz_datamgr:open_cache_doc(AccountDb, Id),
case validate_box_schema(kz_json:set_value(<<"pin">>, Pin, JObj)) of
{'ok', PublicJObj} ->
PrivJObj = kz_doc:private_fields(JObj),
JObj1 = kz_json:merge_jobjs(PrivJObj, PublicJObj),
{'ok', _} = kz_datamgr:save_doc(AccountDb, JObj1),
{'ok', _} = kapps_call_command:b_prompt(<<"vm-pin_set">>, Call),
lager:info("updated mailbox pin number"),
Box;
{'error', _Reason} ->
lager:debug("box failed validation: ~p", [_Reason]),
invalid_pin(Box, Call, Loop)
end
catch
'error':{'badmatch',{'error','channel_hungup'}} ->
lager:debug("channel hungup while configuring pin"),
{'error', 'channel_hungup'};
'error':{'badmatch',{'ok',_ConfirmPin}} ->
lager:debug("new pin was invalid, try again"),
invalid_pin(Box, Call, Loop);
_E:_R ->
lager:debug("failed to get new pin: ~s: ~p", [_E, _R]),
invalid_pin(Box, Call, Loop)
end.
-spec invalid_pin(mailbox(), kapps_call:call(), non_neg_integer()) ->
mailbox() |
{'error', any()}.
invalid_pin(_Box, _Call, Loop) when Loop >= ?MAX_INVALID_PIN_LOOPS ->
lager:debug("several empty or invalid pins"),
{'error', 'max_retry'};
invalid_pin(Box, Call, Loop) ->
case kapps_call_command:b_prompt(<<"vm-pin_invalid">>, Call) of
{'ok', _} -> change_pin(Box, Call, Loop + 1);
{'error', 'channel_hungup'}=E ->
lager:debug("channel hungup after bad pin"),
E;
{'error', _E}=E ->
lager:debug("invalid pin prompt interrupted: ~p", [_E]),
E
end.
-spec validate_box_schema(kz_json:object()) ->
{'ok', kz_json:object()} |
{'error', any()}.
validate_box_schema(JObj) ->
case kz_json_schema:validate(<<"vmboxes">>, kz_doc:public_fields(JObj)) of
{'ok', _}=OK -> OK;
{'error', _Errors} ->
lager:debug("failed to validate vmbox schema: ~p", [_Errors]),
{'error', 'invalid_pin'}
end.
-spec get_new_pin(pos_integer(), kapps_call:call()) ->
{'ok', binary()} |
{'error', any()}.
get_new_pin(Interdigit, Call) ->
NoopId = kapps_call_command:prompt(<<"vm-enter_new_pin">>, Call),
collect_pin(Interdigit, Call, NoopId).
-spec confirm_new_pin(pos_integer(), kapps_call:call()) ->
{'ok', binary()} |
{'error', any()}.
confirm_new_pin(Interdigit, Call) ->
NoopId = kapps_call_command:prompt(<<"vm-enter_new_pin_confirm">>, Call),
collect_pin(Interdigit, Call, NoopId).
-spec collect_pin(pos_integer(), kapps_call:call(), kz_term:ne_binary()) ->
{'ok', binary()} |
{'error', any()}.
collect_pin(Interdigit, Call, NoopId) ->
kapps_call_command:collect_digits(?DEFAULT_MAX_PIN_LENGTH
,kapps_call_command:default_collect_timeout()
,Interdigit
,NoopId
,Call
).
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec new_message(kz_term:ne_binary(), non_neg_integer(), mailbox(), kapps_call:call()) -> any().
new_message(_, Length, #mailbox{min_message_length=MinLength}, _)
when Length < MinLength ->
lager:info("attachment length is ~B and must be larger than ~B to be stored", [Length, MinLength]);
new_message(AttachmentName, Length, #mailbox{mailbox_number=BoxNum
,mailbox_id=BoxId
,timezone=Timezone
,owner_id=OwnerId
,transcribe_voicemail=MaybeTranscribe
,after_notify_action=Action
}=Box, Call) ->
NewMsgProps = [{<<"Box-Id">>, BoxId}
,{<<"Owner-Id">>, OwnerId}
,{<<"Length">>, Length}
,{<<"Transcribe-Voicemail">>, MaybeTranscribe}
,{<<"After-Notify-Action">>, Action}
,{<<"Attachment-Name">>, AttachmentName}
,{<<"Box-Num">>, BoxNum}
,{<<"Timezone">>, Timezone}
],
case kvm_message:new(Call, NewMsgProps) of
{'ok', _NewCall} -> send_mwi_update(Box);
{'error', _, _Msg} -> lager:warning("failed to save voice mail message recorded media : ~p", [_Msg])
end.
%%------------------------------------------------------------------------------
%% @doc Fetches the mailbox parameters from the data store and loads the
%% mailbox record
%% @end
%%------------------------------------------------------------------------------
-spec get_mailbox_profile(kz_json:object(), kapps_call:call()) -> mailbox().
get_mailbox_profile(Data, Call) ->
Id = kz_json:get_ne_binary_value(<<"id">>, Data),
AccountDb = kapps_call:account_db(Call),
case get_mailbox_doc(AccountDb, Id, Data, Call) of
{'ok', MailboxJObj} ->
MailboxId = kz_doc:id(MailboxJObj),
lager:info("loaded voicemail box ~s", [MailboxId]),
Default = #mailbox{},
%% Don't check if the voicemail box belongs to the owner (by default) if the call was not
%% specifically to him, IE: calling a ring group and going to voicemail should not check
LastAct = kapps_call:kvs_fetch('cf_last_action', Call),
CheckIfOwner = (('undefined' =:= LastAct)
orelse ('cf_device' =:= LastAct)
),
{NameMediaId, OwnerId} = owner_info(AccountDb, MailboxJObj),
MaxMessageCount = max_message_count(Call),
MsgCount = kvm_messages:count(kapps_call:account_id(Call), MailboxId),
lager:info("mailbox limited to ~p voicemail messages (has ~b currently)"
,[MaxMessageCount, MsgCount]
),
AfterNotifyAction = after_notify_action(MailboxJObj),
#mailbox{mailbox_id = MailboxId
,exists = 'true'
,keys = populate_keys(Call)
,skip_instructions =
kzd_voicemail_box:skip_instructions(MailboxJObj, Default#mailbox.skip_instructions)
,skip_greeting =
kzd_voicemail_box:skip_greeting(MailboxJObj, Default#mailbox.skip_greeting)
,skip_envelope =
kzd_voicemail_box:skip_envelope(MailboxJObj, Default#mailbox.skip_envelope)
,pin =
kzd_voicemail_box:pin(MailboxJObj, <<>>)
,timezone =
kzd_voicemail_box:timezone(MailboxJObj)
,mailbox_number =
kzd_voicemail_box:mailbox_number(MailboxJObj, kapps_call:request_user(Call))
,require_pin = should_require_pin(MailboxJObj)
,check_if_owner =
kzd_voicemail_box:check_if_owner(MailboxJObj, CheckIfOwner)
,unavailable_media_id =
kz_json:get_ne_value([<<"media">>, <<"unavailable">>], MailboxJObj)
,temporary_unavailable_media_id =
kz_json:get_ne_value([<<"media">>, <<"temporary_unavailable">>], MailboxJObj)
,name_media_id =
NameMediaId
,owner_id =
OwnerId
,is_setup =
kzd_voicemail_box:is_setup(MailboxJObj, 'false')
,max_message_count =
kz_term:to_integer(MaxMessageCount)
,max_message_length = find_max_message_length([Data, MailboxJObj])
,min_message_length = min_recording_length(Call)
,message_count =
MsgCount
,transcribe_voicemail =
kz_json:is_true(<<"transcribe">>, MailboxJObj, 'false')
,notifications =
kz_json:get_json_value(<<"notifications">>, MailboxJObj)
,after_notify_action = AfterNotifyAction
,interdigit_timeout =
kz_json:find(<<"interdigit_timeout">>, [MailboxJObj, Data], kapps_call_command:default_interdigit_timeout())
,play_greeting_intro =
kz_json:is_true(<<"play_greeting_intro">>, MailboxJObj, Default#mailbox.play_greeting_intro)
,use_person_not_available =
kz_json:is_true(<<"use_person_not_available">>, MailboxJObj, Default#mailbox.use_person_not_available)
,not_configurable=
kz_json:is_true(<<"not_configurable">>, MailboxJObj, 'false')
,account_db = AccountDb
,media_extension = kzd_voicemail_box:media_extension(MailboxJObj)
,forward_type = ?DEFAULT_FORWARD_TYPE
};
{'error', R} ->
lager:info("failed to load voicemail box ~s, ~p", [Id, R]),
#mailbox{}
end.
-spec should_require_pin(kz_json:object()) -> boolean().
should_require_pin(MailboxJObj) ->
case ?FORCE_REQUIRE_PIN of
'true' -> 'true';
'false' -> kzd_voicemail_box:pin_required(MailboxJObj)
end.
-spec after_notify_action(kz_json:object()) -> atom().
after_notify_action(MailboxJObj) ->
Delete = kz_json:is_true(?KEY_DELETE_AFTER_NOTIFY, MailboxJObj, ?DEFAULT_DELETE_AFTER_NOTIFY),
Save = kz_json:is_true(?KEY_SAVE_AFTER_NOTIFY, MailboxJObj, ?DEFAULT_SAVE_AFTER_NOTIFY),
case {Delete, Save} of
{'false', 'false'} -> 'nothing';
{'false', 'true'} -> 'save';
{'true', 'false'} -> 'delete';
{'true', 'true'} -> 'save'
end.
-spec max_message_count(kapps_call:call()) -> non_neg_integer().
max_message_count(Call) ->
case kapps_account_config:get(kapps_call:account_id(Call)
,?CF_CONFIG_CAT
,[?KEY_VOICEMAIL, ?KEY_MAX_MESSAGE_COUNT]
)
of
'undefined' -> ?MAILBOX_DEFAULT_SIZE;
MMC -> MMC
end.
-spec owner_info(kz_term:ne_binary(), kz_json:object()) ->
{kz_term:api_binary(), kz_term:api_binary()}.
owner_info(AccountDb, MailboxJObj) ->
owner_info(AccountDb, MailboxJObj, kz_json:get_ne_value(<<"owner_id">>, MailboxJObj)).
-spec owner_info(kz_term:ne_binary(), kz_json:object(), kz_term:api_binary()) ->
{kz_term:api_binary(), kz_term:api_binary()}.
owner_info(_AccountDb, MailboxJObj, 'undefined') ->
{kz_json:get_ne_value(?RECORDED_NAME_KEY, MailboxJObj)
,'undefined'
};
owner_info(AccountDb, MailboxJObj, OwnerId) ->
case kz_datamgr:open_cache_doc(AccountDb, OwnerId) of
{'ok', OwnerJObj} ->
{kz_json:find(?RECORDED_NAME_KEY, [OwnerJObj, MailboxJObj]), OwnerId};
{'error', 'not_found'} ->
lager:info("owner ~s no longer exists", [OwnerId]),
{kz_json:get_ne_value(?RECORDED_NAME_KEY, MailboxJObj), 'undefined'}
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec populate_keys(kapps_call:call()) -> vm_keys().
populate_keys(Call) ->
Default = #keys{},
JObj = kapps_account_config:get_global(kapps_call:account_id(Call), <<"keys">>, ?KEY_VOICEMAIL),
#keys{operator = kz_json:get_binary_value(<<"operator">>, JObj, Default#keys.operator)
,login = kz_json:get_binary_value(<<"login">>, JObj, Default#keys.login)
,save = kz_json:get_binary_value(<<"save">>, JObj, Default#keys.save)
,listen = kz_json:get_binary_value(<<"listen">>, JObj, Default#keys.listen)
,record = kz_json:get_binary_value(<<"record">>, JObj, Default#keys.record)
,hear_new = kz_json:get_binary_value(<<"hear_new">>, JObj, Default#keys.hear_new)
,hear_saved = kz_json:get_binary_value(<<"hear_saved">>, JObj, Default#keys.hear_saved)
,configure = kz_json:get_binary_value(<<"configure">>, JObj, Default#keys.configure)
,exit = kz_json:get_binary_value(<<"exit">>, JObj, Default#keys.exit)
,rec_unavailable = kz_json:get_binary_value(<<"record_unavailable">>, JObj, Default#keys.rec_unavailable)
,rec_name = kz_json:get_binary_value(<<"record_name">>, JObj, Default#keys.rec_name)
,set_pin = kz_json:get_binary_value(<<"set_pin">>, JObj, Default#keys.set_pin)
,return_main = kz_json:get_binary_value(<<"return_main_menu">>, JObj, Default#keys.return_main)
,keep = kz_json:get_binary_value(<<"keep">>, JObj, Default#keys.keep)
,replay = kz_json:get_binary_value(<<"replay">>, JObj, Default#keys.replay)
,prev = kz_json:get_binary_value(<<"prev">>, JObj, Default#keys.prev)
,next = kz_json:get_binary_value(<<"next">>, JObj, Default#keys.next)
,delete = kz_json:get_binary_value(<<"delete">>, JObj, Default#keys.delete)
,continue = kz_json:get_binary_value(<<"continue">>, JObj, Default#keys.continue)
}.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec get_mailbox_doc(kz_term:ne_binary(), kz_term:api_binary(), kz_json:object(), kapps_call:call()) ->
{'ok', kz_json:object()} |
{'error', any()}.
get_mailbox_doc(Db, Id, Data, Call) ->
CaptureGroup = kapps_call:kvs_fetch('cf_capture_group', Call),
CGIsEmpty = kz_term:is_empty(CaptureGroup),
case kz_term:is_empty(Id) of
'false' ->
lager:info("opening ~s", [Id]),
kz_datamgr:open_doc(Db, Id);
'true' when not CGIsEmpty ->
lager:info("capture group not empty: ~s", [CaptureGroup]),
Opts = [{'key', CaptureGroup}
,'include_docs'
,'first_when_multiple'
],
case kz_datamgr:get_single_result(Db, <<"attributes/mailbox_number">>, Opts) of
{'ok', JObj} ->
{'ok', kz_json:get_json_value(<<"doc">>, JObj, kz_json:new())};
E -> E
end;
'true' ->
get_user_mailbox_doc(Data, Call)
end.
-spec get_user_mailbox_doc(kz_json:object(), kapps_call:call()) ->
{'ok', kz_json:object()} |
{'error', any()}.
get_user_mailbox_doc(Data, Call) ->
get_user_mailbox_doc(Data, Call, kapps_call:owner_id(Call)).
-spec get_user_mailbox_doc(kz_json:object(), kapps_call:call(), kz_term:api_binary()) ->
{'ok', kz_json:object()} |
{'error', any()}.
get_user_mailbox_doc(Data, Call, 'undefined') ->
DeviceId = kapps_call:authorizing_id(Call),
case kz_datamgr:open_cache_doc(kapps_call:account_db(Call), DeviceId) of
{'ok', DeviceJObj} ->
case kzd_devices:owner_id(DeviceJObj) of
'undefined' ->
lager:debug("device used to check voicemail has no owner assigned", []),
{'error', "request voicemail box number"};
OwnerId ->
get_user_mailbox_doc(Data, Call, OwnerId)
end;
{'error', _} ->
lager:debug("unknown device used to check voicemail", []),
{'error', "request voicemail box number"}
end;
get_user_mailbox_doc(Data, Call, OwnerId) ->
SingleMailboxLogin = kz_json:is_true(<<"single_mailbox_login">>, Data, 'false'),
case kz_attributes:owned_by_docs(OwnerId, <<"vmbox">>, Call) of
[] ->
lager:debug("owner ~s has no vmboxes", [OwnerId]),
{'error', "request voicemail box number"};
[Box] when SingleMailboxLogin ->
lager:debug("owner ~s has one vmbox ~s, and single mailbox login is enabled"
,[OwnerId, kz_doc:id(Box)]
),
{'ok', Box};
Boxes ->
lager:debug("found ~p vmboxes assigned to owner ~s",
[length(Boxes), OwnerId]),
maybe_match_callerid(Boxes,Data, Call)
end.
-spec maybe_match_callerid(kz_json:objects(), kz_json:object(), kapps_call:call()) ->
{'ok', kz_json:object()} |
{'error', any()}.
maybe_match_callerid(Boxes, Data, Call) ->
case kz_json:is_true(<<"callerid_match_login">>, Data, 'false') of
'false' ->
lager:debug("found voicemail boxes but caller-id match disabled"),
{'error', "request voicemail box number"};
'true' ->
CallerId = kapps_call:caller_id_number(Call),
try_match_callerid(Boxes, CallerId)
end.
-spec try_match_callerid(kz_json:objects(), kz_term:ne_binary()) ->
{'ok', kz_json:object()} |
{'error', any()}.
try_match_callerid([], _CallerId) ->
lager:debug("no voicemail box found for owner with matching caller id ~s", [_CallerId]),
{'error', "request voicemail box number"};
try_match_callerid([Box|Boxes], CallerId) ->
case kz_json:get_ne_binary_value(<<"mailbox">>, Box) of
CallerId ->
lager:debug("found mailbox from caller id ~s", [CallerId]),
{'ok', Box};
_Mailbox ->
try_match_callerid(Boxes, CallerId)
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec review_recording(kz_term:ne_binary(), boolean(), mailbox(), kapps_call:call()) ->
{'ok', 'record' | 'save' | 'no_selection'} |
{'branch', kz_json:object()}.
review_recording(AttachmentName, AllowOperator, Box, Call) ->
review_recording(AttachmentName, AllowOperator, Box, Call, 1).
-spec review_recording(kz_term:ne_binary(), boolean(), mailbox(), kapps_call:call(), integer()) ->
{'ok', 'record' | 'save' | 'no_selection'} |
{'branch', kz_json:object()}.
review_recording(_, _, _, _, Loop) when Loop > 4 ->
{'ok', 'no_selection'};
review_recording(AttachmentName, AllowOperator
,#mailbox{keys=#keys{listen=Listen
,save=Save
,record=Record
,operator=Operator
}
,interdigit_timeout=Interdigit
}=Box
,Call, Loop) ->
lager:info("playing recording review options"),
NoopId = kapps_call_command:prompt(<<"vm-review_recording">>, Call),
case kapps_call_command:collect_digits(?KEY_LENGTH
,kapps_call_command:default_collect_timeout()
,Interdigit
,NoopId
,Call
)
of
{'ok', Listen} ->
lager:info("caller chose to replay the recording"),
_ = kapps_call_command:b_play(AttachmentName, Call),
review_recording(AttachmentName, AllowOperator, Box, Call);
{'ok', Record} ->
lager:info("caller chose to re-record"),
{'ok', 'record'};
{'ok', Save} ->
lager:info("caller chose to save the recording"),
{'ok', 'save'};
{'ok', Operator} when AllowOperator ->
lager:info("caller chose to ring the operator"),
case cf_util:get_operator_callflow(kapps_call:account_id(Call)) of
{'ok', Flow} -> {'branch', Flow};
{'error',_R} -> review_recording(AttachmentName, AllowOperator, Box, Call, Loop + 1)
end;
{'error', 'channel_hungup'} ->
{'ok', 'no_selection'};
{'error', _E} ->
lager:info("error while waiting for review selection ~p", [_E]),
{'ok', 'no_selection'};
_ ->
review_recording(AttachmentName, AllowOperator, Box, Call, Loop + 1)
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec store_recording(kz_term:ne_binary(), non_neg_integer(), kz_term:ne_binary(), mailbox(), kapps_call:call()) -> 'ok' | {'error', kapps_call:call()}.
store_recording(_, Length, _, #mailbox{min_message_length=MinLength}, _)
when Length < MinLength ->
lager:info("attachment length is ~B and must be larger than ~B to be stored", [Length, MinLength]);
store_recording(AttachmentName, _Length, DocId, Box, Call) ->
Url = get_new_attachment_url(AttachmentName, DocId, Box, Call),
Call1 = kapps_call:kvs_store('media_url', Url, Call),
lager:debug("storing recording ~s in doc ~s with url ~s", [AttachmentName, DocId, Url]),
case kapps_call_command:store_file(<<"/tmp/", AttachmentName/binary>>, Url, Call1) of
'ok' -> 'ok';
{'error', _R}=E ->
lager:error("error trying to store media recording: ~p", [_R]),
{'error', kapps_call:kvs_store('error_details', {'error', E}, Call1)}
end.
-spec get_new_attachment_url(kz_term:ne_binary(), kz_term:ne_binary(), mailbox(), kapps_call:call()) ->
kz_term:ne_binary().
get_new_attachment_url(AttachmentName, MediaId, #mailbox{owner_id=OwnerId}, Call) ->
AccountDb = kapps_call:account_db(Call),
_ = case kz_datamgr:open_doc(AccountDb, MediaId) of
{'ok', JObj} ->
maybe_remove_attachments(AccountDb, MediaId, JObj);
{'error', _} -> 'ok'
end,
Opts = props:filter_undefined([{'doc_owner', OwnerId}]),
kz_media_url:store(AccountDb, {<<"media">>, MediaId}, AttachmentName, Opts).
-spec maybe_remove_attachments(kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object()) -> 'ok'.
maybe_remove_attachments(AccountDb, MediaId, JObj) ->
case kz_doc:maybe_remove_attachments(JObj) of
{'false', _} -> 'ok';
{'true', Removed} ->
_ = kz_datamgr:save_doc(AccountDb, Removed),
lager:debug("doc ~s has existing attachments, removing", [MediaId])
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec min_recording_length(kapps_call:call()) -> integer().
min_recording_length(Call) ->
case kapps_account_config:get(kapps_call:account_id(Call)
,?CF_CONFIG_CAT
,[?KEY_VOICEMAIL, ?KEY_MIN_MESSAGE_SIZE]
)
of
'undefined' -> ?MAILBOX_DEFAULT_MSG_MIN_LENGTH;
MML -> kz_term:to_integer(MML)
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec recording_media_doc(kz_term:ne_binary(), mailbox(), kapps_call:call()) -> kz_term:ne_binary().
recording_media_doc(Recording, Box, Call) ->
AccountDb = kapps_call:account_db(Call),
{'ok', JObj} = kz_datamgr:save_doc(AccountDb, set_recording_media_doc(Recording, Box, AccountDb, kz_json:new())),
kz_doc:id(JObj).
-spec maybe_update_recording_media_doc(kz_term:ne_binary(), mailbox(), kapps_call:call(), kz_json:object() | kz_term:ne_binary(), 'new' | 'update') -> 'ok'.
maybe_update_recording_media_doc(_, _, _, _, 'new') ->
'ok';
maybe_update_recording_media_doc(AttachmentName, Box, Call, ?NE_BINARY=MediaId, 'update') ->
update_recording_media_doc(AttachmentName, Box, Call, MediaId);
maybe_update_recording_media_doc(AttachmentName, Box, Call, JObj, 'update') ->
update_recording_media_doc(AttachmentName, Box, Call, kz_doc:id(JObj)).
-spec update_recording_media_doc(kz_term:ne_binary(), mailbox(), kapps_call:call(), kz_term:ne_binary()) -> 'ok'.
update_recording_media_doc(AttachmentName, Box, Call, MediaId) ->
AccountDb = kapps_call:account_db(Call),
case kz_datamgr:open_doc(AccountDb, MediaId) of
{'ok', JObj} ->
case kz_datamgr:save_doc(AccountDb, set_recording_media_doc(AttachmentName, Box, AccountDb, JObj)) of
{'ok', _} -> 'ok';
{'error', _Reason} ->
lager:info("unable to update media_source for media doc ~s in ~s: ~p", [MediaId, AccountDb, _Reason])
end;
{'error', _Reason} ->
lager:info("unable to update media_source for media doc ~s in ~s: ~p", [MediaId, AccountDb, _Reason])
end.
-spec set_recording_media_doc(kz_term:ne_binary(), mailbox(), kz_term:ne_binary(), kz_json:object()) -> kz_json:object().
set_recording_media_doc(Recording, #mailbox{mailbox_number=BoxNum
,mailbox_id=Id
,owner_id=OwnerId
}, AccountDb, JObj) ->
Name = list_to_binary(["mailbox ", BoxNum, " ", Recording]),
Doc = kz_json:set_values(
[{<<"name">>, Name}
,{<<"description">>, <<"voicemail recorded/prompt media">>}
,{<<"source_type">>, ?KEY_VOICEMAIL}
,{<<"source_id">>, Id}
,{<<"owner_id">>, OwnerId}
,{<<"media_source">>, <<"recording">>}
,{<<"streamable">>, 'true'}
], JObj),
kz_doc:update_pvt_parameters(Doc, AccountDb, [{'type', <<"media">>}]).
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec update_doc(kz_json:path() | kz_json:key()
,kz_json:api_json_term()
,mailbox() | kz_term:ne_binary()
,kapps_call:call() | kz_term:ne_binary()
) ->
'ok' |
{'error', atom()}.
update_doc(Key, Value, ?NE_BINARY = Id, ?NE_BINARY = Db) ->
Update = [{Key, Value}],
Updates = [{'update', Update}
,{'ensure_saved', 'true'}
],
case kz_datamgr:update_doc(Db, Id, Updates) of
{'ok', _} -> 'ok';
{'error', _R}=Error ->
lager:info("unable to update ~s in ~s, ~p", [Id, Db, _R]),
Error
end;
update_doc(Key, Value, #mailbox{mailbox_id=Id}, Db) ->
update_doc(Key, Value, Id, Db);
update_doc(Key, Value, Id, Call) ->
update_doc(Key, Value, Id, kapps_call:account_db(Call)).
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec tmp_file(kz_term:ne_binary()) -> kz_term:ne_binary().
tmp_file(Ext) ->
<<(kz_binary:rand_hex(16))/binary, ".", Ext/binary>>.
%%------------------------------------------------------------------------------
%% @doc Accepts Universal Coordinated Time (UTC) and convert it to binary
%% encoded Unix epoch in the provided timezone
%% @end
%%------------------------------------------------------------------------------
-spec get_unix_epoch(kz_time:gregorian_seconds(), kz_term:ne_binary()) ->
kz_term:ne_binary().
get_unix_epoch(Epoch, Timezone) ->
UtcDateTime = calendar:gregorian_seconds_to_datetime(Epoch),
LocalDateTime = localtime:utc_to_local(UtcDateTime, Timezone),
kz_term:to_binary(calendar:datetime_to_gregorian_seconds(LocalDateTime) - ?UNIX_EPOCH_IN_GREGORIAN).
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec find_max_message_length(kz_json:objects()) -> pos_integer().
find_max_message_length([]) -> ?MAILBOX_DEFAULT_MSG_MAX_LENGTH;
find_max_message_length([JObj | T]) ->
case kz_json:get_integer_value(?KEY_MAX_MESSAGE_LENGTH, JObj) of
Len when is_integer(Len)
andalso Len > 0 -> Len;
_ -> find_max_message_length(T)
end.
-spec is_owner(kapps_call:call(), kz_term:ne_binary()) -> boolean().
is_owner(Call, OwnerId) ->
case kapps_call:owner_id(Call) of
<<>> -> 'false';
'undefined' -> 'false';
OwnerId -> 'true';
_Else -> 'false'
end.
-spec send_mwi_update(mailbox()) -> 'ok'.
send_mwi_update(#mailbox{mailbox_id='undefined'}) -> 'ok';
send_mwi_update(#mailbox{account_db=AccountDb
,mailbox_id=BoxId
}) ->
kvm_mwi:notify_vmbox(AccountDb, BoxId).