%%%----------------------------------------------------------------------------- %%% @copyright (C) 2013-2019, 2600Hz %%% @doc Pickup a call in the specified group. %%% %%%

Data options:

%%%
%%%
`approved_device_id'
%%%
Device ID.
%%% %%%
`approved_user_id'
%%%
User ID.
%%% %%%
`approved_group_id'
%%%
Group ID.
%%%
%%% %%% One of the three, `group_id', `user_id' or `owner_id', must be defined on %%% the data payload. Preference is given by most restrictive option set, %%% so `device_id' is checked for first, then `user_id', and finally `group_id'. %%% %%% `device_id' will only steal a channel currently ringing that device %%% `user_id' will only steal a channel currently ringing any of the user's devices*. %%% `group_id' will steal a channel from any devices or users in a group*. %%% %%% * No guarantees on which if multiple inbound calls are ringing. %%% %%% %%% @author James Aimonetti %%% @end %%%----------------------------------------------------------------------------- -module(cf_group_pickup). -behaviour(gen_cf_action). -include("callflow.hrl"). -export([handle/2]). %%------------------------------------------------------------------------------ %% @doc Entry point for this module sends an arbitrary response back to the %% call originator. %% @end %%------------------------------------------------------------------------------ -spec handle(kz_json:object(), kapps_call:call()) -> any(). handle(Data, Call) -> _ = case maybe_allowed_to_intercept(Data, Call) of 'true' -> case find_sip_endpoints(Data, Call) of [] -> no_users_in_group(Call); DeviceIds -> connect_to_ringing_channel(DeviceIds, Call) end; 'false' -> no_permission_to_intercept(Call) end, cf_exe:stop(Call). -spec maybe_allowed_to_intercept(kz_json:object(), kapps_call:call()) -> boolean(). maybe_allowed_to_intercept(Data, Call) -> case kz_json:get_ne_binary_value(<<"approved_device_id">>, Data) of 'undefined' -> case kz_json:get_ne_binary_value(<<"approved_user_id">>, Data) of 'undefined' -> case kz_json:get_ne_binary_value(<<"approved_group_id">>, Data) of 'undefined' -> 'true'; GroupId -> maybe_belongs_to_group(GroupId, Call) end; UserId -> maybe_belongs_to_user(UserId, Call) end; DeviceId -> %% Compare approved device_id with calling one DeviceId == kapps_call:authorizing_id(Call) end. -spec maybe_belongs_to_user(kz_term:ne_binary(), kapps_call:call()) -> boolean(). maybe_belongs_to_user(UserId, Call) -> lists:member(kapps_call:authorizing_id(Call), find_user_endpoints([UserId],[],Call)). -spec maybe_belongs_to_group(kz_term:ne_binary(), kapps_call:call()) -> boolean(). maybe_belongs_to_group(GroupId, Call) -> lists:member(kapps_call:authorizing_id(Call), find_group_endpoints(GroupId, Call)). -spec connect_to_ringing_channel(kz_term:ne_binaries(), kapps_call:call()) -> 'ok'. connect_to_ringing_channel(DeviceIds, Call) -> _ = case find_channels(DeviceIds) of [] -> no_channels_ringing(Call); Channels -> connect_to_a_channel(Channels, Call) end, 'ok'. -spec connect_to_a_channel(kz_json:objects(), kapps_call:call()) -> 'ok'. connect_to_a_channel(Channels, Call) -> MyUUID = kapps_call:call_id(Call), MyMediaServer = kapps_call:switch_nodename(Call), lager:debug("looking for channels on my node ~s that aren't me", [MyMediaServer]), case sort_channels(Channels, MyUUID, MyMediaServer) of {[], []} -> lager:debug("no channels available to pickup"), no_channels_ringing(Call); {[], [RemoteUUID|_Remote]} -> lager:debug("no unanswered calls on my media server, trying ~s", [RemoteUUID]), intercept_call(RemoteUUID, Call); {[LocalUUID|_Cs], _} -> lager:debug("found a call (~s) on my media server", [LocalUUID]), intercept_call(LocalUUID, Call) end. -spec sort_channels(kz_json:objects(), kz_term:ne_binary(), kz_term:ne_binary()) -> {kz_term:ne_binaries(), kz_term:ne_binaries()}. sort_channels(Channels, MyUUID, MyMediaServer) -> sort_channels(Channels, MyUUID, MyMediaServer, {[], []}). -spec sort_channels(kz_json:objects(), kz_term:ne_binary(), kz_term:ne_binary(), {kz_term:ne_binaries(), kz_term:ne_binaries()}) -> {kz_term:ne_binaries(), kz_term:ne_binaries()}. sort_channels([], _MyUUID, _MyMediaServer, Acc) -> Acc; sort_channels([Channel|Channels], MyUUID, MyMediaServer, Acc) -> lager:debug("channel: c: ~s a: ~s n: ~s oleg: ~s", [kz_json:get_ne_binary_value(<<"uuid">>, Channel) ,kz_json:is_true(<<"answered">>, Channel) ,kz_json:get_ne_binary_value(<<"node">>, Channel) ,kz_json:get_ne_binary_value(<<"other_leg">>, Channel) ]), case kz_json:is_true(<<"answered">>, Channel) of 'true' -> sort_channels(Channels, MyUUID, MyMediaServer, Acc); 'false' -> maybe_add_unanswered_leg(Channels, MyUUID, MyMediaServer, Acc, Channel) end. -spec maybe_add_unanswered_leg(kz_json:objects(), kz_term:ne_binary(), kz_term:ne_binary(), {kz_term:ne_binaries(), kz_term:ne_binaries()}, kz_json:object()) -> {kz_term:ne_binaries(), kz_term:ne_binaries()}. maybe_add_unanswered_leg(Channels, MyUUID, MyMediaServer, {Local, Remote}=Acc, Channel) -> case kz_json:get_ne_binary_value(<<"node">>, Channel) of MyMediaServer -> case kz_json:get_ne_binary_value(<<"uuid">>, Channel) of MyUUID -> sort_channels(Channels, MyUUID, MyMediaServer, Acc); _UUID -> sort_channels(Channels, MyUUID, MyMediaServer, {maybe_add_other_leg(Channel, Local), Remote}) end; _OtherMediaServer -> sort_channels(Channels, MyUUID, MyMediaServer, {Local, maybe_add_other_leg(Channel, Remote)}) end. -spec maybe_add_other_leg(kz_json:object(), kz_term:ne_binaries()) -> kz_term:ne_binaries(). maybe_add_other_leg(Channel, Legs) -> case kz_json:get_ne_binary_value(<<"other_leg">>, Channel) of 'undefined' -> Legs; Leg -> [Leg | Legs] end. -spec intercept_call(kz_term:ne_binary(), kapps_call:call()) -> 'ok'. intercept_call(UUID, Call) -> _ = kapps_call_command:send_command(pickup_cmd(UUID), Call), case wait_for_pickup(Call) of {'error', _E} -> lager:debug("failed to pickup ~s: ~p", [UUID, _E]); 'ok' -> lager:debug("call picked up"), _ = kapps_call_command:wait_for_hangup(), lager:debug("hangup recv") end. -spec pickup_cmd(kz_term:ne_binary()) -> kz_term:proplist(). pickup_cmd(TargetCallId) -> [{<<"Application-Name">>, <<"call_pickup">>} ,{<<"Target-Call-ID">>, TargetCallId} ,{<<"Unbridged-Only">>, 'true'} ]. -spec wait_for_pickup(kapps_call:call()) -> 'ok' | {'error', 'failed'} | {'error', 'timeout'}. wait_for_pickup(Call) -> case kapps_call_command:receive_event(10000) of {'ok', Evt} -> pickup_event(Call, kz_util:get_event_type(Evt), Evt); {'error', 'timeout'}=E -> lager:debug("timed out"), E end. -spec pickup_event(kapps_call:call(), {kz_term:ne_binary(), kz_term:ne_binary()}, kz_json:object()) -> {'error', 'failed' | 'timeout'} | 'ok'. pickup_event(_Call, {<<"error">>, <<"dialplan">>}, Evt) -> lager:debug("error in dialplan: ~s", [kz_json:get_ne_binary_value(<<"Error-Message">>, Evt)]), {'error', 'failed'}; pickup_event(_Call, {<<"call_event">>,<<"CHANNEL_BRIDGE">>}, _Evt) -> lager:debug("channel bridged to ~s", [kz_json:get_ne_binary_value(<<"Other-Leg-Call-ID">>, _Evt)]); pickup_event(Call, _Type, _Evt) -> lager:debug("unhandled evt ~p", [_Type]), wait_for_pickup(Call). -spec find_channels(kz_term:ne_binaries()) -> kz_json:objects(). find_channels(DeviceIds) -> lager:debug("finding channels for devices ids ~p", [DeviceIds]), Req = [{<<"Authorizing-IDs">>, DeviceIds} ,{<<"Active-Only">>, 'false'} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ], case kz_amqp_worker:call_collect(Req ,fun kapi_call:publish_query_user_channels_req/1 ,{'ecallmgr', 'true'} ) of {'error', _E} -> lager:debug("failed to get channels: ~p", [_E]), []; {_, JObjs} -> lists:foldl(fun(JObj, Channels) -> kz_json:get_value(<<"Channels">>, JObj, []) ++ Channels end, [], JObjs) end. -spec find_sip_endpoints(kz_json:object(), kapps_call:call()) -> kz_term:ne_binaries(). find_sip_endpoints(Data, Call) -> case kz_json:get_ne_binary_value(<<"device_id">>, Data) of 'undefined' -> case kz_json:get_ne_binary_value(<<"user_id">>, Data) of 'undefined' -> find_sip_users(kz_json:get_ne_binary_value(<<"group_id">>, Data), Call); UserId -> find_user_endpoints([UserId], [], Call) end; DeviceId -> [DeviceId] end. -spec find_sip_users(kz_term:api_binary(), kapps_call:call()) -> kz_term:ne_binaries(). find_sip_users(GroupId, Call) when is_binary(GroupId) -> find_group_endpoints(GroupId, Call). -spec find_group_endpoints(kz_term:ne_binary(), kapps_call:call()) -> kz_term:ne_binaries(). find_group_endpoints(GroupId, Call) -> GroupsJObj = kz_attributes:groups(Call), case [kz_json:get_value(<<"value">>, JObj) || JObj <- GroupsJObj, kz_doc:id(JObj) =:= GroupId ] of [] -> []; [GroupEndpoints] -> Ids = kz_json:get_keys(GroupEndpoints), find_endpoints(Ids, GroupEndpoints, Call) end. -spec find_endpoints(kz_term:ne_binaries(), kz_json:object(), kapps_call:call()) -> kz_term:ne_binaries(). find_endpoints(Ids, GroupEndpoints, Call) -> {DeviceIds, UserIds} = lists:partition(fun(Id) -> kz_json:get_ne_binary_value([Id, <<"type">>], GroupEndpoints) =:= <<"device">> end, Ids), find_user_endpoints(UserIds, lists:sort(DeviceIds), Call). -spec find_user_endpoints(kz_term:ne_binaries(), kz_term:ne_binaries(), kapps_call:call()) -> kz_term:ne_binaries(). find_user_endpoints([], DeviceIds, _) -> DeviceIds; find_user_endpoints(UserIds, DeviceIds, Call) -> UserDeviceIds = kz_attributes:owned_by(UserIds, <<"device">>, Call), lists:merge(lists:sort(UserDeviceIds), DeviceIds). -spec no_users_in_group(kapps_call:call()) -> any(). no_users_in_group(Call) -> kapps_call_command:answer(Call), kapps_call_command:b_play(<<"system_media/pickup-no_users">>, Call). -spec no_channels_ringing(kapps_call:call()) -> any(). no_channels_ringing(Call) -> kapps_call_command:answer(Call), kapps_call_command:b_play(<<"system_media/pickup-no_channels">>, Call). -spec no_permission_to_intercept(kapps_call:call()) -> any(). %% TODO: please convert to system_media file (say is not consistent on deployments) no_permission_to_intercept(Call) -> kapps_call_command:answer(Call), kapps_call_command:b_say(<<"you have no permission to intercept this call">>, Call).