%%%-----------------------------------------------------------------------------
%%% @copyright (C) 2010-2019, 2600Hz
%%% @doc Provide functions to create and manage a single voicemail message.
%%% @author Hesaam Farhang
%%% @end
%%%-----------------------------------------------------------------------------
-module(kvm_message).
-export([new/2, forward_message/4
,fetch/2, fetch/3, message/2, message/3
,set_folder/3, change_folder/4, change_folder/5
,update/3, update/4
,move_to_vmbox/4, move_to_vmbox/5, maybe_do_move/7
,copy_to_vmboxes/4, copy_to_vmboxes/5, maybe_copy_to_vmboxes/7
,media_url/2
]).
-include("kz_voicemail.hrl").
-export_type([vm_folder/0]).
-type new_msg_ret() :: {'ok', kapps_call:call()} | {'error', kapps_call:call(), kz_term:text()}.
%% Result of storing message. If it's not successful element 3 of tuple has the error message.
%%------------------------------------------------------------------------------
%% @doc Receives and stores a new voicemail message.
%% Usually this function is called by {@link cf_voicemail} module to create message
%% metadata and store the media file in the storage. This may results in
%% sending a notification to the the owner of the mailbox or device if it was
%% requested by mailbox owner by setting `delete_after_notify' or `save_after_notify' in
%% the mailbox document.
%%
%% Options are:
%%
%%
%% - `{<<"Attachment-Name">>, '{@link kz_term:ne_binary()}`}'
%% - Media file name
%% - `{<<"Box-Id">>, '{@link kz_term:ne_binary()}`}'
%% - The mailbox ID the message is belong to
%% - `{<<"OwnerId">>, '{@link kz_term:ne_binary()}`}'
%% - The owner ID of the mailbox
%% - `{<<"Length">>, integer()}'
%% - Media file size (or audio duration?)
%% - `{<<"Transcribe-Voicemail">>, boolean()}'
%% - Should try to transcribe the message with external service
%% - `{<<"After-Notify-Action">>, '{@link notify_action()}`}'
%% - The action to execute if sending notification was successful
%% - `{<<"Box-Num">>, '{@link kz_term:ne_binary()}`}'
%% - Extension or phone number of the mailbox
%% - `{<<"Timezone">>, '{@link kz_term:api_binary()}`}'
%% - Configured timezone of the mailbox or device or user or account. If it
%% is `undefined' system default timezone will be used instead.
%%
%% @end
%%------------------------------------------------------------------------------
-spec new(Call, Options) -> Result when Call::kapps_call:call(),
Options::kz_term:proplist(),
Result::new_msg_ret().
new(Call, Options) ->
BoxId = props:get_value(<<"Box-Id">>, Options),
%% FIXME: dis guy is file size not audio duration
Length = props:get_value(<<"Length">>, Options),
AttachmentName = props:get_value(<<"Attachment-Name">>, Options),
lager:debug("saving new ~bms voicemail media and metadata", [Length]),
case create_new_message_doc(Call, Options) of
{'error', _} ->
Msg = io_lib:format("failed to create and save voicemail document for voicemail box ~s of account ~s"
,[BoxId, kapps_call:account_id(Call)]
),
{'error', Call, Msg};
{'ok', MessageDoc} ->
MediaUrl = fun() -> kz_media_url:store(MessageDoc, AttachmentName) end,
MessageId = kz_doc:id(MessageDoc),
Msg = io_lib:format("failed to store voicemail media ~s in voicemail box ~s of account ~s"
,[MessageId, BoxId, kapps_call:account_id(Call)]
),
Funs = [{fun kapps_call:kvs_store/3, 'mailbox_id', BoxId}
,{fun kapps_call:kvs_store/3, 'attachment_name', AttachmentName}
,{fun kapps_call:kvs_store/3, 'media_id', MessageId}
,{fun kapps_call:kvs_store/3, 'media_length', Length}
],
lager:debug("storing voicemail media recording ~s in doc ~s", [AttachmentName, MessageId]),
case store_recording(AttachmentName, MediaUrl, kapps_call:exec(Funs, Call), MessageId) of
'ok' ->
notify_and_update_meta(Call, MessageId, Length, Options);
{'error', Call1} ->
lager:error(Msg),
{'error', Call1, Msg}
end
end.
%%------------------------------------------------------------------------------
%% @doc Forwards and stores a voicemail message from source mailbox into destination mailbox.
%% Usually this function is called by {@link cf_voicemail} module to forward a message from its source
%% mailbox into the destination mailbox. This may result in sending a notification as described in {@link new/2}.
%%
%% For `Options' description see {@link new/2}.
%%
%% If the callee did record a message (if `Attachment-Name' is present in the Options), it will tries to append
%% the forwarding message to to the callee's message. If it failed the original forwarding message will be save
%% into the destination mailbox.
%%
%% @see new/2
%% @end
%%------------------------------------------------------------------------------
-spec forward_message(Call, Metadata, SrcBoxId, Options) ->
Result when Call::kapps_call:call(),
Metadata::kz_json:object(),
SrcBoxId::kz_term:ne_binary(),
Options::kz_term:proplist(),
Result::new_msg_ret().
forward_message(Call, Metadata, SrcBoxId, Options) ->
case props:get_value(<<"Attachment-Name">>, Options) of
'undefined' ->
%% user chose to forward without prepending
forward_to_vmbox(Call, Metadata, SrcBoxId, Options);
_AttachmentName ->
%% user chose to forward and prepend a message
new_forward_message(Call, Metadata, SrcBoxId, Options)
end.
-spec new_forward_message(kapps_call:call(), kz_json:object(), kz_term:ne_binary(), kz_term:proplist()) -> new_msg_ret().
new_forward_message(Call, Metadata, SrcBoxId, Options) ->
DestBoxId = props:get_value(<<"Box-Id">>, Options),
Length = props:get_value(<<"Length">>, Options),
AttachmentName = props:get_value(<<"Attachment-Name">>, Options),
lager:debug("saving new ~bms forward voicemail media and metadata", [Length]),
case create_forward_message_doc(Call, Metadata, SrcBoxId, Options) of
{'error', _} ->
Msg = io_lib:format("failed to create and save voicemail document for forwarded message ~s to voicemail box ~s of account ~s"
,[kzd_box_message:media_id(Metadata), DestBoxId, kapps_call:account_id(Call)]
),
{'error', Call, Msg};
{ForwardId, MediaUrl} ->
Msg = io_lib:format("failed to store forward voicemail media ~s in voicemail box ~s of account ~s"
,[ForwardId, DestBoxId, kapps_call:account_id(Call)]
),
Funs = [{fun kapps_call:kvs_store/3, 'dest_mailbox_id', DestBoxId}
,{fun kapps_call:kvs_store/3, 'attachment_name', AttachmentName}
,{fun kapps_call:kvs_store/3, 'media_id', ForwardId}
,{fun kapps_call:kvs_store/3, 'media_length', Length}
],
lager:debug("storing forward voicemail media recording ~s in doc ~s", [AttachmentName, ForwardId]),
case store_recording(AttachmentName, MediaUrl, kapps_call:exec(Funs, Call), ForwardId) of
'ok' ->
prepend_and_notify(Call, ForwardId, Metadata, SrcBoxId, Options);
{'error', Call1} ->
lager:error(Msg),
{'error', Call1, Msg}
end
end.
%% @equiv fetch(AccountId, MessageId, 'undefined')
-spec fetch(AccountId, MessageId) -> db_ret() when AccountId::kz_term:ne_binary(), MessageId::kz_term:ne_binary().
fetch(AccountId, MessageId) ->
fetch(AccountId, MessageId, 'undefined').
%%------------------------------------------------------------------------------
%% @doc Fetch a message document while considering the retention policy.
%% `MessageId` is in `MODB_PREFIX' format (`YYYYMM-...'). If the message is older than
%% account's voicemail retention policy, it will marked as deleted.
%%
%% Since this function can be called by Crossbar, the message is checked that it is belonged
%% to the specified mailbox ID or not. If not `{error, not_found}' will be returned.
%% @end
%%------------------------------------------------------------------------------
-spec fetch(AccountId, MessageId, BoxId) -> db_ret() when AccountId::kz_term:ne_binary(),
MessageId::kz_term:ne_binary(),
BoxId::kz_term:api_ne_binary().
fetch(AccountId, MessageId, BoxId) ->
RetenTimestamp = kz_time:now_s() - kvm_util:retention_seconds(AccountId),
{_, DbRet} = do_fetch(AccountId, MessageId, BoxId, RetenTimestamp),
DbRet.
-spec do_fetch(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:api_ne_binary(), kz_time:gregorian_seconds()) -> {boolean(), db_ret()}.
do_fetch(AccountId, MessageId, BoxId, RetenTimestamp) ->
case kvm_util:open_modb_doc(AccountId, MessageId, kzd_box_message:type()) of
{'ok', JObj} ->
IsPrior = kvm_util:is_prior_to_retention(JObj, RetenTimestamp),
case kvm_util:check_msg_belonging(BoxId, JObj) of
'false' -> {'false', {'error', 'not_found'}};
'true' when IsPrior ->
{'true', {'ok', kvm_util:enforce_retention(JObj, 'true')}};
'true' ->
{'false', {'ok', JObj}}
end;
{'error', _E} = Error ->
lager:debug("failed to open message ~s:~p", [MessageId, _E]),
{'false', Error}
end.
%% @equiv message(AccountId, MessageId, 'undefined')
-spec message(AccountId, MessageId) -> db_ret() when AccountId::kz_term:ne_binary(), MessageId::kz_term:ne_binary().
message(AccountId, MessageId) ->
message(AccountId, MessageId, 'undefined').
%%------------------------------------------------------------------------------
%% @doc Fetch message metadata while considering the retention policy.
%% See {@link fetch/2} for description about retention policy.
%%
%% @see fetch/2
%% @end
%%------------------------------------------------------------------------------
-spec message(AccountId, MessageId, BoxId) -> db_ret() when AccountId::kz_term:ne_binary(),
MessageId::kz_term:ne_binary(),
BoxId::kz_term:api_ne_binary().
message(AccountId, MessageId, BoxId) ->
case fetch(AccountId, MessageId, BoxId) of
{'ok', JObj} ->
{'ok', kzd_box_message:metadata(JObj)};
Error -> Error
end.
%%------------------------------------------------------------------------------
%% @doc Change the message's folder.
%% Returns the new updated message on success or the old message if update failed.
%%
%% For use by {@link cf_voicemail} only.
%% @end
%%------------------------------------------------------------------------------
-spec set_folder(Folder, Message, AccountId) -> db_ret() when Folder::kz_term:ne_binary(),
Message::kz_json:object(),
AccountId::kz_term:ne_binary().
set_folder(Folder, Message, AccountId) ->
MessageId = kzd_box_message:media_id(Message),
FromFolder = kzd_box_message:folder(Message, ?VM_FOLDER_NEW),
lager:info("setting folder for message ~s to ~p", [MessageId, Folder]),
case maybe_set_folder(FromFolder, Folder, MessageId, AccountId, Message) of
{'ok', _} = OK -> OK;
{'error', _} -> {'error', Message}
end.
-spec maybe_set_folder(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object()) -> db_ret().
maybe_set_folder(_, ?VM_FOLDER_DELETED = ToFolder, MessageId, AccountId, _Msg) ->
%% ensuring that message is really deleted
change_folder(ToFolder, MessageId, AccountId, 'undefined');
maybe_set_folder(FromFolder, FromFolder, _MessageId, _AccountId, Msg) ->
{'ok', Msg};
maybe_set_folder(_FromFolder, ToFolder, MessageId, AccountId, _Msg) ->
change_folder(ToFolder, MessageId, AccountId, 'undefined').
%% @equiv change_folder(Folder, Message, AccountId, BoxId, [])
-spec change_folder(Folder, Message, AccountId, BoxId) -> db_ret() when Folder::vm_folder(),
Message::message(),
AccountId::kz_term:ne_binary(),
BoxId::kz_term:api_binary().
change_folder(Folder, Message, AccountId, BoxId) ->
change_folder(Folder, Message, AccountId, BoxId, []).
%%------------------------------------------------------------------------------
%% @doc Change the message's folder.
%% If `Folder' is `` {<<"deleted">>, 'true'} '', the message
%% would move to deleted folder and and its document will marked as soft-deleted,
%% otherwise it just move to deleted folder (for recovering later by user).
%% @end
%%------------------------------------------------------------------------------
-spec change_folder(Folder, Message, AccountId, BoxId, Functions) ->
db_ret() when Folder::vm_folder(),
Message::message(),
AccountId::kz_term:ne_binary(),
BoxId::kz_term:api_binary(),
Functions::update_funs().
change_folder(Folder, Message, AccountId, BoxId, Funs0) ->
Funs = [fun(J) -> kzd_box_message:apply_folder(Folder, J) end
| Funs0
],
case update(AccountId, BoxId, Message, Funs) of
{'ok', JObj} ->
{'ok', kzd_box_message:metadata(JObj)};
{'error', _R} = Error ->
lager:debug("failed to update message ~s folder to ~s: ~p", [Folder, _R]),
Error
end.
%% @equiv update(AccountId, BoxId, Message, [])
-spec update(kz_term:ne_binary(), kz_term:api_ne_binary(), message()) -> db_ret().
update(AccountId, BoxId, Message) ->
update(AccountId, BoxId, Message, []).
%%------------------------------------------------------------------------------
%% @doc Update the message document.
%% It tries to fetch the message and applies provided function on the document. You can pass a JObj
%% instead of `MessageId'.
%%
%% If the message is prior to retention policy the message is marked as deleted in database
%% and error `{error, <<"prior_to_retention_duration">>}' will be returned instead.
%% @end
%%------------------------------------------------------------------------------
-spec update(AccountId, BoxId, Message, Functions) ->
db_ret() when AccountId::kz_term:ne_binary(),
BoxId::kz_term:api_ne_binary(),
Message::message(),
Functions::update_funs().
update(AccountId, BoxId, ?NE_BINARY = MsgId, Funs) ->
RetenTimestamp = kz_time:now_s() - kvm_util:retention_seconds(AccountId),
case do_fetch(AccountId, MsgId, BoxId, RetenTimestamp) of
{'true', {'ok', JObj}} ->
_ = do_update(JObj, [fun(J) -> kvm_util:enforce_retention(J, 'true') end]),
{'error', <<"prior_to_retention_duration">>};
{'false', {'ok', JObj}} ->
do_update(JObj, Funs);
{_, {'error', _}=Error} ->
Error
end;
update(AccountId, _BoxId, JObj, Funs) ->
RetenTimestamp = kz_time:now_s() - kvm_util:retention_seconds(AccountId),
case kvm_util:is_prior_to_retention(JObj, RetenTimestamp) of
'true' ->
_ = do_update(JObj, [fun(J) -> kvm_util:enforce_retention(J, 'true') end]),
{'error', <<"prior_to_retention_duration">>};
'false' ->
do_update(JObj, Funs)
end.
-spec do_update(kz_json:object(), update_funs()) -> db_ret().
do_update(JObj, Funs) ->
NewJObj = lists:foldl(fun(F, J) -> F(J) end, JObj, Funs),
case try_save_document('undefined', NewJObj, 3) of
{'ok', _}=OK -> OK;
{'error', _R}=Error ->
lager:debug("failed to update voicemail message ~s: ~p", [kz_doc:id(NewJObj), _R]),
Error
end.
%% @equiv move_to_vmbox(AccountId, Things, OldBoxId, NewBoxId, [])
-spec move_to_vmbox(AccountId, Message, OldBoxId, NewBoxId) ->
db_ret() when AccountId::kz_term:ne_binary(),
Message::message(),
OldBoxId::kz_term:ne_binary(),
NewBoxId::kz_term:ne_binary().
move_to_vmbox(AccountId, Things, OldBoxId, NewBoxId) ->
move_to_vmbox(AccountId, Things, OldBoxId, NewBoxId, []).
%%------------------------------------------------------------------------------
%% @doc Moves a message to another mailbox.
%% It reads the mailbox document from database first, then calls {@link maybe_do_move/7}.
%%
%% @see maybe_do_move/7
%% @end
%%------------------------------------------------------------------------------
-spec move_to_vmbox(AccountId, Message, OldBoxId, NewBoxId, Functions) ->
db_ret() when AccountId::kz_term:ne_binary(),
Message::message(),
OldBoxId::kz_term:ne_binary(),
NewBoxId::kz_term:ne_binary(),
Functions::update_funs().
move_to_vmbox(AccountId, ?NE_BINARY = FromId, OldBoxId, NewBoxId, Funs) ->
AccountDb = kvm_util:get_db(AccountId),
case kz_datamgr:open_cache_doc(AccountDb, NewBoxId) of
{'ok', NBoxJ} ->
RetenTimestamp = kz_time:now_s() - kvm_util:retention_seconds(AccountId),
maybe_do_move(AccountId, FromId, OldBoxId, NewBoxId, NBoxJ, Funs, RetenTimestamp);
{'error', _Reason} = Error ->
lager:debug("failed to open destination vmbox ~s: ~p", [NewBoxId, _Reason]),
Error
end;
move_to_vmbox(AccountId, JObj, OldBoxId, NewBoxId, Funs) ->
move_to_vmbox(AccountId, kzd_box_message:get_msg_id(JObj), OldBoxId, NewBoxId, Funs).
%%------------------------------------------------------------------------------
%% @doc Moves a message to another mailbox.
%% If the message is prior to retention policy it will marked as deleted and
%% `{error, <<"prior_to_retention_duration">>}' will returned instead.
%%
%% It calls by {@link kvm_messages:move_to_vmbox/5}
%% @end
%%------------------------------------------------------------------------------
-spec maybe_do_move(AccountId, MessageId, OldBoxId, NewBoxId, NewBoxJObj, Functions, RetenTimestamp) ->
db_ret() when AccountId::kz_term:ne_binary(),
MessageId::kz_term:ne_binary(),
OldBoxId::kz_term:ne_binary(),
NewBoxId::kz_term:ne_binary(),
NewBoxJObj::kz_json:object(),
Functions::update_funs(),
RetenTimestamp::kz_time:gregorian_seconds().
maybe_do_move(AccountId, FromId, OldBoxId, NewBoxId, NBoxJ, Funs, RetenTimestamp) ->
case do_fetch(AccountId, FromId, OldBoxId, RetenTimestamp) of
{'true', {'ok', JObj}} ->
_ = do_update(JObj, [fun(J) -> kvm_util:enforce_retention(J, 'true') end]),
{'error', <<"prior_to_retention_duration">>};
{'false', {'ok', _}} ->
do_move(AccountId, FromId, OldBoxId, NewBoxId, NBoxJ, Funs);
{_, {'error', _}=Error} ->
Error
end.
-spec do_move(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object(), update_funs()) -> db_ret().
do_move(AccountId, FromId, OldBoxId, NewBoxId, NBoxJ, Funs) ->
{ToId, TransformFuns} = kvm_util:get_change_vmbox_funs(AccountId, NewBoxId, NBoxJ, OldBoxId),
FromDb = kvm_util:get_db(AccountId, FromId),
ToDb = kvm_util:get_db(AccountId, ToId),
lager:debug("moving voicemail ~s/~s (vmbox ~s) to ~s/~s (vmbox ~s)"
,[FromDb, FromId, OldBoxId, ToDb, ToId, NewBoxId]
),
Opts = [{'transform', fun(_, B) -> lists:foldl(fun(F, J) -> F(J) end, B, TransformFuns ++ Funs) end}
,{'max_retries', 3}
],
case kazoo_modb:move_doc(FromDb, {kzd_box_message:type(), FromId}, ToDb, ToId, Opts) of
{'ok', _} = OK -> OK;
{'error', 'timeout'} ->
move_copy_final_check(AccountId, FromId, ToId);
{'error', 'conflict'} ->
Msg = io_lib:format("conflict occurred during moving voicemail ~s / ~s to ~s / ~s"
,[FromDb, FromId, ToDb, ToId]
),
Subject = <<"Conflict during forward voicemail message">>,
send_system_alert('undefined', AccountId, Subject, kz_term:to_binary(Msg)),
move_copy_final_check(AccountId, FromId, ToId);
{'error', _} = Error ->
lager:debug("failed to move ~s/~s to ~s/~s", [FromDb, FromId, ToDb, ToId]),
Error
end.
%% @equiv copy_to_vmboxes(AccountId, MsgThing, OldBoxId, NewBoxIds, [])
-spec copy_to_vmboxes(AccountId, Message, OldBoxId, NewBoxIds) ->
kz_json:object() when AccountId::kz_term:ne_binary(),
Message::message(),
OldBoxId::kz_term:ne_binary(),
NewBoxIds::kz_term:ne_binary() | kz_term:ne_binaries().
copy_to_vmboxes(AccountId, MsgThing, OldBoxId, NewBoxIds) ->
copy_to_vmboxes(AccountId, MsgThing, OldBoxId, NewBoxIds, []).
%%------------------------------------------------------------------------------
%% @doc Copy a message to other mailbox(es)
%% If the message is prior to retention policy it will marked as deleted and
%% `{error, <<"prior_to_retention_duration">>}' will returned instead.
%%
%% Returns a JObj in the below form:
%% ```
%% {[{<<"succeeded">>
%% ,[<<"some_id">>]
%% }
%% ,{<<"failed">>
%% ,[{<<"some_id">>, <<"some_reason">>}]
%% }
%% ]}
%% '''
%% @end
%%------------------------------------------------------------------------------
-spec copy_to_vmboxes(AccountId, MessageId, OldBoxId, NewBoxIds, Functions) ->
kz_json:object() when AccountId::kz_term:ne_binary(),
MessageId::message(),
OldBoxId::kz_term:ne_binary(),
NewBoxIds::kz_term:ne_binary() | kz_term:ne_binaries(),
Functions::update_funs().
copy_to_vmboxes(AccountId, MessageId, OldBoxId, ?NE_BINARY = NewBoxId, Funs) ->
copy_to_vmboxes(AccountId, MessageId, OldBoxId, [NewBoxId], Funs);
copy_to_vmboxes(AccountId, ?NE_BINARY = MessageId, OldBoxId, NewBoxIds, Funs) ->
RetenTimestamp = kz_time:now_s() - kvm_util:retention_seconds(AccountId),
kz_json:from_list_recursive(
maps:to_list(
maybe_copy_to_vmboxes(AccountId, MessageId, OldBoxId, NewBoxIds, #{}, Funs, RetenTimestamp)
)
);
copy_to_vmboxes(AccountId, JObj, OldBoxId, NewBoxIds, Funs) ->
copy_to_vmboxes(AccountId, kzd_box_message:get_msg_id(JObj), OldBoxId, NewBoxIds, Funs).
%%------------------------------------------------------------------------------
%% @doc Copy a message to other mailbox(es)
%% If the message is prior to retention policy it will marked as deleted and
%% `{error, <<"prior_to_retention_duration">>}' will returned instead.
%%
%% It calls by {@link kvm_messages:copy_to_vmboxes/5}
%% @end
%%------------------------------------------------------------------------------
-spec maybe_copy_to_vmboxes(AccountId, FromId, OldBoxId, NewBoxIds, Acc, Functions, RetenTimestamp) ->
bulk_map() when AccountId::kz_term:ne_binary(),
FromId::kz_term:ne_binary(),
OldBoxId::kz_term:ne_binary(),
NewBoxIds::kz_term:ne_binaries(),
Acc::bulk_map(),
Functions::update_funs(),
RetenTimestamp::kz_time:gregorian_seconds().
maybe_copy_to_vmboxes(AccountId, FromId, OldBoxId, NewBoxIds, CopyMap, Funs, RetenTimestamp) ->
case do_fetch(AccountId, FromId, OldBoxId, RetenTimestamp) of
{'true', {'ok', JObj}} ->
_ = do_update(JObj, [fun(J) -> kvm_util:enforce_retention(J, 'true') end]),
IdReason = {FromId, <<"prior_to_retention_duration">>},
maps:update_with(failed, fun(List) -> [IdReason|List] end, [IdReason], CopyMap);
{'false', {'ok', _}} ->
copy_to_vmboxes(AccountId, FromId, OldBoxId, NewBoxIds, CopyMap, Funs);
{_, {'error', Reason}} ->
IdReason = {FromId, kz_term:to_binary(Reason)},
maps:update_with(failed, fun(List) -> [IdReason|List] end, [IdReason], CopyMap)
end.
-spec copy_to_vmboxes(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binaries(), bulk_map(), update_funs()) -> bulk_map().
copy_to_vmboxes(_, _, _, [], CopyMap, _) ->
CopyMap;
copy_to_vmboxes(AccountId, FromId, OldBoxId, [NBId | NBIds], CopyMap, Funs) ->
NewCopyMap = copy_to_vmbox(AccountId, FromId, OldBoxId, NBId, CopyMap, Funs),
copy_to_vmboxes(AccountId, FromId, OldBoxId, NBIds, NewCopyMap, Funs).
-spec copy_to_vmbox(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), bulk_map(), update_funs()) -> bulk_map().
copy_to_vmbox(AccountId, ?NE_BINARY = FromId, OldBoxId, ?NE_BINARY = NBId, CopyMap, Funs) ->
AccountDb = kvm_util:get_db(AccountId),
copy_to_vmbox(AccountId, FromId, OldBoxId, NBId, CopyMap
,kz_datamgr:open_cache_doc(AccountDb, NBId)
,Funs
).
-spec copy_to_vmbox(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), bulk_map(), kz_datamgr:data_error() | {'ok', kz_json:object()}, update_funs()) ->
bulk_map().
copy_to_vmbox(_AccountId, FromId, _OldBoxId, NBId, CopyMap
,{'error', Reason}
,_
) ->
lager:warning("could not open destination vmbox ~s", [NBId]),
IdReason = {FromId, kz_term:to_binary(Reason)},
maps:update_with(failed, fun(List) -> [IdReason|List] end, [IdReason], CopyMap);
copy_to_vmbox(AccountId, FromId, OldBoxId, NBId, CopyMap
,{'ok', NBox}
,Funs
) ->
{ToId, TransformFuns} = kvm_util:get_change_vmbox_funs(AccountId, NBId, NBox, OldBoxId),
case do_copy(AccountId, FromId, ToId, TransformFuns ++ Funs) of
{'ok', CopiedJObj} ->
CopiedId = kz_doc:id(CopiedJObj),
maps:update_with('succeeded', fun(List) -> [CopiedId|List] end, [CopiedId], CopyMap);
{'error', R} ->
IdReason = {FromId, kz_term:to_binary(R)},
maps:update_with('failed', fun(List) -> [IdReason|List] end, [IdReason], CopyMap)
end.
-spec do_copy(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), update_funs()) -> db_ret().
do_copy(AccountId, ?NE_BINARY = FromId, ToId, Funs) ->
FromDb = kvm_util:get_db(AccountId, FromId),
ToDb = kvm_util:get_db(AccountId, ToId),
lager:debug("copying voicemail ~s/~s to ~s/~s"
,[FromDb, FromId, ToDb, ToId]
),
Opts = [{'transform', fun(_, B) -> lists:foldl(fun(F, J) -> F(J) end, B, Funs) end}
,{'max_retries', 3}
],
case kazoo_modb:copy_doc(FromDb, {kzd_box_message:type(), FromId}, ToDb, ToId, Opts) of
{'ok', _} = OK -> OK;
{'error', 'timeout'} ->
move_copy_final_check(AccountId, FromId, ToId);
{'error', 'conflict'} ->
Msg = io_lib:format("conflict occurred during forwarding voicemail ~s / ~s to ~s / ~s"
,[FromDb, FromId, ToDb, ToId]
),
Subject = <<"Conflict during forward voicemail message">>,
send_system_alert('undefined', AccountId, Subject, kz_term:to_binary(Msg)),
move_copy_final_check(AccountId, FromId, ToId);
{'error', _}=Error ->
lager:debug("failed to copy ~s/~s to ~s/~s", [FromDb, FromId, ToDb, ToId]),
Error
end.
-spec move_copy_final_check(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> db_ret().
move_copy_final_check(AccountId, FromId, ToId) ->
FromDb = kvm_util:get_db(AccountId, FromId),
ToDb = kvm_util:get_db(AccountId, ToId),
case fetch(AccountId, ToId) of
{'ok', _}=OK -> OK; %% message was saved somehow(network glitch?), moving on
{'error', _} ->
lager:error("max retries to copy or move voicemail message ~s/~s to ~s/~s"
,[FromDb, FromId, ToDb, ToId]
),
{'error', 'max_save_retries'}
end.
%%------------------------------------------------------------------------------
%% @doc Get Url of the media file from media server.
%% @end
%%------------------------------------------------------------------------------
-spec media_url(AccountId, Message) -> binary() when AccountId::kz_term:ne_binary(), Message::message().
media_url(AccountId, ?NE_BINARY = MessageId) ->
case fetch(AccountId, MessageId) of
{'ok', Message} ->
case kz_media_url:playback(Message, Message) of
{'error', _} -> <<>>;
Url -> Url
end;
{'error', _} -> <<>>
end;
media_url(AccountId, Message) ->
media_url(AccountId, kzd_box_message:media_id(Message)).
%%%=============================================================================
%%% Internal functions
%%%=============================================================================
-type store_media_url() :: fun(() -> kz_term:ne_binary() | {'error', any()}).
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec create_new_message_doc(kapps_call:call(), kz_term:proplist()) ->
{'ok', kzd_box_message:doc()} |
{'error', any()}.
create_new_message_doc(Call, Props) ->
AccountId = kapps_call:account_id(Call),
JObj = kzd_box_message:new(AccountId, Props),
Length = props:get_value(<<"Length">>, Props),
CIDNumber = kvm_util:get_caller_id_number(Call),
CIDName = kvm_util:get_caller_id_name(Call),
Timestamp = kz_time:now_s(),
Metadata = kzd_box_message:build_metadata_object(Length, Call, kz_doc:id(JObj), CIDNumber, CIDName, Timestamp),
MsgJObj = kzd_box_message:set_metadata(Metadata, JObj),
try_save_document(Call, MsgJObj, 3).
-spec create_forward_message_doc(kapps_call:call(), kz_json:object(), kz_term:ne_binary(), kz_term:proplist()) ->
{kz_term:ne_binary(), store_media_url()} |
{'error', any()}.
create_forward_message_doc(Call, Metadata, SrcBoxId, Props) ->
AccountId = kapps_call:account_id(Call),
JObj = kzd_box_message:set_metadata(Metadata, kzd_box_message:new(AccountId, Props)),
NewBoxJObj = fake_vmbox_jobj(Call, Props),
{_NewId, VMChangeFuns} = kvm_util:get_change_vmbox_funs(kapps_call:account_id(Call)
,kz_doc:id(NewBoxJObj)
,NewBoxJObj
,SrcBoxId
,kz_doc:id(JObj)
),
Updates = [fun(M) -> kz_json:set_value(<<"length">>, props:get_value(<<"Length">>, Props), M) end
| VMChangeFuns
],
MsgJObj = lists:foldl(fun(F, J) -> F(J) end, JObj, Updates),
AttachmentName = props:get_value(<<"Attachment-Name">>, Props),
case try_save_document(Call, MsgJObj, 3) of
{'ok', SavedJObj} ->
MediaUrl = fun() -> kz_media_url:store(SavedJObj, AttachmentName) end,
{kz_doc:id(SavedJObj), MediaUrl};
{'error', _}=Error -> Error
end.
-spec try_save_document('undefined' | kapps_call:call(), kz_json:object(), 1..3) -> db_ret().
try_save_document(_Call, MsgJObj, 0) ->
case fetch(kz_doc:account_id(MsgJObj), kz_doc:id(MsgJObj)) of
{'ok', _}=OK -> OK; %% message was saved somehow(network glitch?), moving on
{'error', _} ->
lager:error("max retries to save voicemail message ~s in db ~s"
,[kz_doc:id(MsgJObj), kz_doc:account_db(MsgJObj)]
),
{'error', 'max_save_retries'}
end;
try_save_document(Call, MsgJObj, Loop) ->
case kazoo_modb:save_doc(kz_doc:account_db(MsgJObj), MsgJObj, [{'max_retries', 3}]) of
{'ok', _}=OK -> OK;
{'error', 'conflict'} ->
RetryFun = fun(J) -> try_save_document(Call, J, Loop - 1) end,
maybe_retry_conflict(Call, MsgJObj, RetryFun);
{'error', _Reason}=Error ->
lager:error("failed to save voicemail message ~s in db ~s : ~p"
,[kz_doc:id(MsgJObj), kz_doc:account_db(MsgJObj), _Reason]
),
Error
end.
%%------------------------------------------------------------------------------
%% @doc create a fake Destination Box JObj to pass to change vmbox functions
%%
%% Set `pvt_account_id' and db just to make sure for case
%% when timezone is not passed so {@link kzd_voicemail_box} can find
%% timezone from vmbox the owner or account.
%% @end
%%------------------------------------------------------------------------------
-spec fake_vmbox_jobj(kapps_call:call(), kz_term:proplist()) -> kz_json:object().
fake_vmbox_jobj(Call, Props) ->
kz_json:from_list([{<<"_id">>, props:get_value(<<"Box-Id">>, Props)}
,{<<"mailbox">>, props:get_value(<<"Box-Num">>, Props)}
,{<<"timezone">>, props:get_value(<<"Timezone">>, Props)}
,{<<"owner_id">>, props:get_value(<<"Owner-Id">>, Props)}
,{<<"pvt_account_id">>, kapps_call:account_id(Call)}
,{<<"pvt_account_db">>, kapps_call:account_db(Call)}
]
).
-spec store_recording(kz_term:ne_binary(), kz_term:ne_binary() | store_media_url(), kapps_call:call(), kz_term:ne_binary()) ->
'ok' |
{'error', kapps_call:call()}.
store_recording(AttachmentName, Url, Call, MessageId) ->
case kapps_call_command:store_file(<<"/tmp/", AttachmentName/binary>>, Url, Call) of
'ok' -> lager:debug("stored ~s to ~s", [AttachmentName, Url]);
{'error', _R} ->
lager:warning("error during storing voicemail recording ~s , checking attachment existence: ~p", [MessageId, _R]),
check_attachment_exists(Call, MessageId)
end.
-spec check_attachment_exists(kapps_call:call(), kz_term:ne_binary()) -> 'ok' | {'error', kapps_call:call()}.
check_attachment_exists(Call, MessageId) ->
case fetch(kapps_call:account_id(Call), MessageId) of
{'ok', JObj} ->
case kz_term:is_empty(kz_doc:attachments(JObj)) of
'true' ->
{'error', Call};
'false' ->
lager:debug("freeswitch returned error during store voicemail recording, but attachments is saved anyway")
end;
{'error', _R} ->
lager:warning("failed to check attachment existence doc id ~s: ~p", [MessageId, _R]),
{'error', Call}
end.
-spec forward_to_vmbox(kapps_call:call(), kz_json:object(), kz_term:ne_binary(), kz_term:proplist()) -> new_msg_ret().
forward_to_vmbox(Call, Metadata, SrcBoxId, Props) ->
forward_to_vmbox(Call, Metadata, SrcBoxId, Props, []).
-spec forward_to_vmbox(kapps_call:call(), kz_json:object(), kz_term:ne_binary(), kz_term:proplist(), update_funs()) -> new_msg_ret().
forward_to_vmbox(Call, Metadata, SrcBoxId, Props, Funs) ->
AccountId = kapps_call:account_id(Call),
MediaId = kzd_box_message:media_id(Metadata),
DestBoxId = props:get_value(<<"Box-Id">>, Props),
Length = props:get_value(<<"Length">>, Props),
ResultMap = copy_to_vmbox(AccountId, MediaId, SrcBoxId, DestBoxId, #{}
,kz_datamgr:open_cache_doc(kz_util:format_account_db(AccountId), DestBoxId)
,Funs
),
Failed = maps:get(failed, ResultMap, []),
Succeeded = maps:get(succeeded, ResultMap, []),
case {Failed, Succeeded} of
{[], []} -> {'error', Call, 'internal_error'};
{[], [ForwardId]} ->
%%TODO: update length and caller_id
notify_and_update_meta(Call, ForwardId, Length, Props);
{[{_Id, Reason}], _} -> {'error', Call, Reason}
end.
-spec prepend_and_notify(kapps_call:call(), kz_term:ne_binary(), kz_json:object(), kz_term:ne_binary(), kz_term:proplist()) -> new_msg_ret().
prepend_and_notify(Call, ForwardId, Metadata, SrcBoxId, Props) ->
Length = props:get_value(<<"Length">>, Props),
try prepend_forward_message(Call, ForwardId, Metadata, SrcBoxId, Props) of
{'ok', _} ->
%%TODO: update length and caller_id
notify_and_update_meta(Call, ForwardId, Length, Props);
{'error', Reason} ->
%% prepend failed, so at least try to forward without a prepend message
remove_malform_vm(Call, ForwardId),
ErrorMessage = kz_term:to_binary(io_lib:format("failed to prepend and joining audio files: ~p", [Reason])),
UpdateFuns = [fun(J) -> kz_json:set_value(<<"forward_join_error">>, ErrorMessage, J) end],
forward_to_vmbox(Call, Metadata, SrcBoxId, Props, UpdateFuns)
catch
_T:_E ->
remove_malform_vm(Call, ForwardId),
ST = erlang:get_stacktrace(),
ErrorMessage = kz_term:to_binary(io_lib:format("exception occurred during prepend and joining audio files: ~p:~p", [_T, _E])),
lager:error(ErrorMessage),
kz_util:log_stacktrace(ST),
%% prepend failed, so at least try to forward without a prepend message
UpdateFuns = [fun(J) -> kz_json:set_value(<<"forward_join_error">>, ErrorMessage, J) end],
forward_to_vmbox(Call, Metadata, SrcBoxId, Props, UpdateFuns)
end.
-spec prepend_forward_message(kapps_call:call(), kz_term:ne_binary(), kz_json:object(), kz_term:ne_binary(), kz_term:proplist()) -> db_ret().
prepend_forward_message(Call, ForwardId, Metadata, _SrcBoxId, Props) ->
lager:debug("trying to prepend a message to forwarded voicemail message ~s", [ForwardId]),
AccountId = kapps_call:account_id(Call),
lager:debug("saving prepend message ~s attachment to file system", [ForwardId]),
TmpAttachmentName = props:get_ne_binary_value(<<"Attachment-Name">>, Props),
{'ok', TmpPath} = write_attachment_to_file(AccountId, ForwardId, [TmpAttachmentName]),
{'ok', _} = kz_datamgr:delete_attachment(kvm_util:get_db(AccountId, ForwardId), ForwardId, TmpAttachmentName),
OrigMsgId = kzd_box_message:media_id(Metadata),
lager:debug("saving original message ~s attachment to file system", [OrigMsgId]),
{'ok', OrigPath} = write_attachment_to_file(AccountId, OrigMsgId),
{'ok', OrigSampleRate} = kz_media_util:detect_file_sample_rate(OrigPath),
TonePath = kz_binary:join([<<"/tmp/">>, <<(kz_binary:rand_hex(16))/binary, ".wav">>], <<>>),
{'ok', _} = kz_media_util:synthesize_tone(OrigSampleRate, <<"440">>, <<"0.5">>, TonePath),
lager:debug("joining prepend to original message"),
case kz_media_util:join_media_files([TmpPath, TonePath, OrigPath], [{sample_rate, OrigSampleRate}]) of
{'ok', FileContents} ->
JoinFilename = <<(kz_binary:rand_hex(16))/binary, ".mp3">>,
_ = [kz_util:delete_file(F) || F <- [TmpPath, OrigPath, TonePath]],
%%TODO: update forwarded doc with length and media_filename
try_put_fwd_attachment(AccountId, ForwardId, JoinFilename, FileContents, 3);
{'error', _} ->
_ = [kz_util:delete_file(F) || F <- [TmpPath, OrigPath, TonePath]],
lager:warning("failed to join forward message media files"),
{'error', 'join_failed'}
end.
-spec try_put_fwd_attachment(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), iodata(), 1..3) -> db_ret().
try_put_fwd_attachment(AccountId, ForwardId, _JoinFilename, _FileContents, 0) ->
lager:error("max retries to save prepend forward voicemail attachment ~s in db ~s"
,[ForwardId, kvm_util:get_db(AccountId, ForwardId)]
),
{'error', 'max_save_retries'};
try_put_fwd_attachment(AccountId, ForwardId, JoinFilename, FileContents, Loop) ->
case kz_datamgr:put_attachment(kvm_util:get_db(AccountId, ForwardId), ForwardId, JoinFilename, FileContents) of
{'ok', _}=OK -> OK;
{'error', 'conflict'} ->
try_put_fwd_attachment(AccountId, ForwardId, JoinFilename, FileContents, Loop - 1);
{'error', 'timeout'} ->
try_put_fwd_attachment(AccountId, ForwardId, JoinFilename, FileContents, Loop - 1);
{'error', _Reason}=Error ->
lager:error("failed to save prepend forward voicemail message ~s in db ~s : ~p"
,[ForwardId, kvm_util:get_db(AccountId, ForwardId), _Reason]),
Error
end.
-spec write_attachment_to_file(kz_term:ne_binary(), kz_term:ne_binary()) -> {'ok', kz_term:ne_binary()} | {'error', any()}.
write_attachment_to_file(AccountId, MessageId) ->
case fetch(AccountId, MessageId) of
{'ok', Doc} ->
write_attachment_to_file(AccountId, MessageId, kz_doc:attachment_names(Doc));
{'error', _}=Error -> Error
end.
-spec write_attachment_to_file(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binaries()) -> {'ok', kz_term:ne_binary()} | {'error', any()}.
write_attachment_to_file(AccountId, MessageId, [AttachmentId]) ->
Db = kvm_util:get_db(AccountId, MessageId),
{'ok', AttachmentBin} = kz_datamgr:fetch_attachment(Db, MessageId, AttachmentId),
FilePath = kz_binary:join([<<"/tmp/_">>, AttachmentId], <<>>),
kz_util:write_file(FilePath, AttachmentBin, ['write', 'binary']),
lager:debug("saved attachment ~s from ~s in ~s", [AttachmentId, MessageId, FilePath]),
{'ok', FilePath}.
-spec remove_malform_vm(kapps_call:call(), kz_term:ne_binary()) -> 'ok'.
remove_malform_vm(Call, ForwardId) ->
AccountDb = kz_util:format_account_db(kapps_call:account_id(Call)),
_ = kz_datamgr:del_doc(AccountDb, ForwardId),
'ok'.
-type notify_action() :: 'save' | 'delete' | 'nothing'.
-spec notify_and_update_meta(kapps_call:call(), kz_term:ne_binary(), integer(), kz_term:proplist()) -> {'ok', kapps_call:call()}.
notify_and_update_meta(Call, MediaId, Length, Props) ->
BoxId = props:get_value(<<"Box-Id">>, Props),
NotifyAction = props:get_atom_value(<<"After-Notify-Action">>, Props, 'nothing'),
case kvm_util:publish_saved_notify(MediaId, BoxId, Call, Length, Props) of
{'ok', JObjs} ->
NewAction = is_notified_successfully(Call, MediaId, JObjs, NotifyAction),
maybe_update_meta(Length, NewAction, Call, MediaId, BoxId);
{'timeout', JObjs} ->
NewAction = is_notified_successfully(Call, MediaId, JObjs, NotifyAction),
maybe_update_meta(Length, NewAction, Call, MediaId, BoxId);
{'error', _R} ->
AccountId = kapps_call:account_id(Call),
lager:debug("failed to send new voicemail notification for message ~s in account ~s: ~p"
,[MediaId, AccountId, _R]
),
maybe_update_meta(Length, 'nothing', Call, MediaId, BoxId)
end.
%%------------------------------------------------------------------------------
%% @doc If notification was successfully processed return the NotifyAction.
%% Otherwise return action 'nothing' to store the message as new voicemail.
%% @end
%%------------------------------------------------------------------------------
-spec is_notified_successfully(kapps_call:call(), kz_term:ne_binary(), kz_json:objects(), notify_action()) -> notify_action().
is_notified_successfully(Call, _MediaId, [], _) ->
lager:debug("failed to send new voicemail notification for message ~s in account ~s: timeout", [_MediaId, kapps_call:account_id(Call)]),
'nothing';
is_notified_successfully(Call, MediaId, [JObj|JObjs], NotifyAction) ->
case kz_json:get_ne_binary_value(<<"Status">>, JObj) of
<<"completed">> -> NotifyAction;
<<"disabled">> -> 'nothing';
<<"ignored">> -> 'nothing';
<<"failed">> -> 'nothing';
_Status -> is_notified_successfully(Call, MediaId, JObjs, NotifyAction)
end.
-spec maybe_update_meta(pos_integer(), notify_action(), kapps_call:call(), kz_term:ne_binary(), kz_term:ne_binary()) -> {'ok', kapps_call:call()}.
maybe_update_meta(Length, Action, Call, MediaId, BoxId) ->
case Action of
'delete' ->
lager:debug("attachment was sent out via notification, set folder to delete"),
Fun = [fun(JObj) ->
kzd_box_message:apply_folder({?VM_FOLDER_DELETED, 'false'}, JObj)
end
],
update_metadata(Call, BoxId, MediaId, Fun);
'save' ->
lager:debug("attachment was sent out via notification, set folder to saved"),
Fun = [fun(JObj) ->
kzd_box_message:apply_folder(?VM_FOLDER_SAVED, JObj)
end
],
update_metadata(Call, BoxId, MediaId, Fun);
'nothing' ->
Timestamp = kz_time:now_s(),
kvm_util:publish_voicemail_saved(Length, BoxId, Call, MediaId, Timestamp),
{'ok', Call}
end.
-spec update_metadata(kapps_call:call(), kz_term:ne_binary(), kz_term:ne_binary(), update_funs()) -> {'ok', kapps_call:call()}.
update_metadata(Call, BoxId, MessageId, UpdateFuns) ->
AccountId = kapps_call:account_id(Call),
case update(AccountId, BoxId, MessageId, UpdateFuns) of
{'ok', _} -> {'ok', Call};
{'error', _R} ->
lager:info("error while updating voicemail metadata: ~p", [_R]),
{'ok', Call}
end.
%%------------------------------------------------------------------------------
%% @doc Double check document to make sure it's not exists in db.
%% Using `kz_datamgr:ensure_save` is more efficient here, but
%% we're doing this fetch/retry for proof of concept whether document's id
%% had collision or not.
%% @end
%%------------------------------------------------------------------------------
-spec maybe_retry_conflict(kapps_call:call() | 'undefined', kz_json:object(), fun((kz_json:object()) -> db_ret())) -> db_ret().
maybe_retry_conflict(Call, JObj, DieAnotherDay) ->
case fetch(kz_doc:account_id(JObj), kz_doc:id(JObj)) of
{'ok', SavedJObj} -> check_for_collision(Call, JObj, SavedJObj, DieAnotherDay);
{'error', 'timeout'} -> DieAnotherDay(JObj);
{'error', 'not_found'} -> DieAnotherDay(JObj);
{'error', _}=Error -> Error
end.
-spec check_for_collision(kapps_call:call() | 'undefined', kz_json:object(), kz_json:object(), fun((kz_json:object()) -> db_ret())) -> db_ret().
check_for_collision(Call, JObj, SavedJObj, DieAnotherDay) ->
OrigPublic = kz_doc:public_fields(JObj),
SavedPublic = kz_doc:public_fields(SavedJObj),
case kz_json:are_equal(OrigPublic, SavedPublic) of
'true' ->
Msg = io_lib:format("saving new voicemail ~s in account ~p resulted in conflict but it saved to db anyway"
,[kz_doc:id(JObj), kz_doc:account_id(JObj)]
),
Subject = <<"Conflict during saving new voicemail message">>,
send_system_alert(Call, kz_doc:account_id(JObj), Subject, kz_term:to_binary(Msg)),
{'ok', SavedJObj};
'false' ->
Msg = io_lib:format("found document id collision during saving a new voicemail, id ~s account_id ~p"
,[kz_doc:id(JObj), kz_doc:account_id(JObj)]
),
lager:critical(Msg),
Subject = <<"Document ID collision detected">>,
send_system_alert(Call, kz_doc:account_id(JObj), Subject, kz_term:to_binary(Msg)),
NewId = give_me_another_id(kz_doc:id(JObj)),
NewJObj = kzd_box_message:update_media_id(NewId, JObj),
DieAnotherDay(kz_doc:set_id(NewJObj, NewId))
end.
-spec give_me_another_id(kz_term:ne_binary()) -> kz_term:ne_binary().
give_me_another_id(?MATCH_MODB_PREFIX(Year, Month, _)) ->
kazoo_modb_util:modb_id(Year, Month).
-spec send_system_alert(kapps_call:call() | 'undefined', kz_term:api_ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'.
send_system_alert('undefined', AccountId, Subject, Msg) ->
Notify = [{<<"Message">>, Msg}
,{<<"Subject">>, <<"System Alert: ", Subject/binary>>}
,{<<"Account-ID">>, AccountId}
| kz_api:default_headers(?APP_VERSION, ?APP_NAME)
],
kz_amqp_worker:cast(Notify, fun kapi_notifications:publish_system_alert/1);
send_system_alert(Call, AccountId, Subject, Msg) ->
Notify = [{<<"Message">>, Msg}
,{<<"Subject">>, <<"System Alert: ", Subject/binary>>}
,{<<"Details">>, kapps_call:to_json(Call)}
,{<<"Account-ID">>, AccountId}
| kz_api:default_headers(?APP_VERSION, ?APP_NAME)
],
kz_amqp_worker:cast(Notify, fun kapi_notifications:publish_system_alert/1).