diff --git a/applications/crossbar/priv/api/swagger.json b/applications/crossbar/priv/api/swagger.json index 48898f5a109..9f0d5f95048 100644 --- a/applications/crossbar/priv/api/swagger.json +++ b/applications/crossbar/priv/api/swagger.json @@ -23515,6 +23515,9 @@ "Force-Fax": { "type": "string" }, + "From": { + "type": "string" + }, "From-Realm": { "type": "string" }, @@ -23605,6 +23608,9 @@ "Timeout": { "type": "string" }, + "To": { + "type": "string" + }, "To-Realm": { "type": "string" }, @@ -23819,6 +23825,9 @@ "Account-ID": { "type": "string" }, + "Application-ID": { + "type": "string" + }, "Body": { "type": "string" }, @@ -23891,6 +23900,12 @@ "Orig-Port": { "type": "string" }, + "Originator-Flags": { + "type": "string" + }, + "Originator-Properties": { + "type": "string" + }, "Request": { "type": "string" }, @@ -23916,6 +23931,12 @@ "System-ID": { "type": "string" }, + "Target-Flags": { + "type": "string" + }, + "Target-Properties": { + "type": "string" + }, "To": { "type": "string" }, @@ -30530,6 +30551,11 @@ "description": "doodle inbound queue name", "type": "string" }, + "listeners": { + "default": 1, + "description": "doodle listeners", + "type": "integer" + }, "min_bucket_cost": { "default": 1, "description": "doodle minimum bucket cost", @@ -32029,6 +32055,25 @@ }, "type": "object" }, + "system_config.kazoo_im": { + "description": "Schema for kazoo_im system_config", + "properties": { + "connector": { + "properties": { + "connections": { + "description": "kazoo_im connector connections", + "type": "object" + } + } + }, + "onnet_listeners": { + "default": 1, + "description": "kazoo_im onnet_listeners", + "type": "integer" + } + }, + "type": "object" + }, "system_config.keys": { "description": "Schema for DTMF keys system_config", "properties": { diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.sms.message.json b/applications/crossbar/priv/couchdb/schemas/kapi.sms.message.json index b53fec96784..d049f85cc6a 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.sms.message.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.sms.message.json @@ -116,6 +116,9 @@ "Force-Fax": { "type": "string" }, + "From": { + "type": "string" + }, "From-Realm": { "type": "string" }, @@ -206,6 +209,9 @@ "Timeout": { "type": "string" }, + "To": { + "type": "string" + }, "To-Realm": { "type": "string" }, diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.sms.outbound.json b/applications/crossbar/priv/couchdb/schemas/kapi.sms.outbound.json index 5dd9137c974..9d45d4dbcde 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.sms.outbound.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.sms.outbound.json @@ -6,6 +6,9 @@ "Account-ID": { "type": "string" }, + "Application-ID": { + "type": "string" + }, "Body": { "type": "string" }, @@ -78,6 +81,12 @@ "Orig-Port": { "type": "string" }, + "Originator-Flags": { + "type": "string" + }, + "Originator-Properties": { + "type": "string" + }, "Request": { "type": "string" }, @@ -103,6 +112,12 @@ "System-ID": { "type": "string" }, + "Target-Flags": { + "type": "string" + }, + "Target-Properties": { + "type": "string" + }, "To": { "type": "string" }, diff --git a/applications/crossbar/priv/couchdb/schemas/system_config.doodle.json b/applications/crossbar/priv/couchdb/schemas/system_config.doodle.json index d3cb5021bda..b0bbc9c851f 100644 --- a/applications/crossbar/priv/couchdb/schemas/system_config.doodle.json +++ b/applications/crossbar/priv/couchdb/schemas/system_config.doodle.json @@ -47,6 +47,11 @@ "description": "doodle inbound queue name", "type": "string" }, + "listeners": { + "default": 1, + "description": "doodle listeners", + "type": "integer" + }, "min_bucket_cost": { "default": 1, "description": "doodle minimum bucket cost", diff --git a/applications/crossbar/priv/couchdb/schemas/system_config.kazoo_im.json b/applications/crossbar/priv/couchdb/schemas/system_config.kazoo_im.json new file mode 100644 index 00000000000..e70dbb2f91d --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/system_config.kazoo_im.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "system_config.kazoo_im", + "description": "Schema for kazoo_im system_config", + "properties": { + "connector": { + "properties": { + "connections": { + "description": "kazoo_im connector connections", + "type": "object" + } + } + }, + "onnet_listeners": { + "default": 1, + "description": "kazoo_im onnet_listeners", + "type": "integer" + } + }, + "type": "object" +} diff --git a/applications/crossbar/src/cb_context.erl b/applications/crossbar/src/cb_context.erl index 1a665167e9d..6c15026856a 100644 --- a/applications/crossbar/src/cb_context.erl +++ b/applications/crossbar/src/cb_context.erl @@ -30,6 +30,7 @@ %% Getters / Setters ,setters/2 + ,validators/2 ,new/0 ,account_id/1, set_account_id/2 @@ -461,6 +462,23 @@ setters_fold({F, V}, C) -> F(C, V); setters_fold({F, K, V}, C) -> F(C, K, V); setters_fold(F, C) when is_function(F, 1) -> F(C). +%%------------------------------------------------------------------------------ +%% @doc Loop over a list of functions and values to validate `cb_context()'. +%% @end +%%------------------------------------------------------------------------------ +-spec validators(context(), setters()) -> context(). +validators(#cb_context{}=Context, []) -> Context; +validators(#cb_context{}=Context, [_|_]=Setters) -> + validators_fold(Context, Setters). + +-spec validators_fold(context(), setters()) -> context(). +validators_fold(Context, []) -> Context; +validators_fold(Context, [Setter | Setters]) -> + NewContext = setters_fold(Setter, Context), + case resp_status(NewContext) of + 'success' -> validators_fold(NewContext, Setters); + 'error' -> NewContext + end. -spec set_account_id(context(), kz_term:ne_binary()) -> context(). set_account_id(#cb_context{}=Context, AcctId) -> diff --git a/applications/crossbar/src/modules/cb_sms.erl b/applications/crossbar/src/modules/cb_sms.erl index 7067d9dfefd..7414c233262 100644 --- a/applications/crossbar/src/modules/cb_sms.erl +++ b/applications/crossbar/src/modules/cb_sms.erl @@ -106,12 +106,12 @@ validate_sms(Context, Id, ?HTTP_DELETE) -> %%------------------------------------------------------------------------------ -spec put(cb_context:context()) -> cb_context:context(). put(Context) -> - Doc = cb_context:doc(Context), - case kazoo_modb:save_doc(kz_doc:account_db(Doc), Doc) of - {'ok', Saved} -> crossbar_util:response(Saved, Context); - {'error', Error} -> - crossbar_doc:handle_datamgr_errors(Error, kz_doc:id(Doc), Context) - end. + Payload = cb_context:doc(Context), + _ = case kz_api_sms:route_type(Payload) of + <<"onnet">> -> kz_amqp_worker:cast(Payload, fun kapi_sms:publish_inbound/1); + <<"offnet">> -> kz_amqp_worker:cast(Payload, fun kapi_sms:publish_outbound/1) + end, + crossbar_util:response(kz_api:remove_defaults(Payload), Context). %%------------------------------------------------------------------------------ %% @doc If the HTTP verb is DELETE, execute the actual action, usually a db delete @@ -147,12 +147,20 @@ read(Id, Context) -> %%------------------------------------------------------------------------------ -spec on_successful_validation(cb_context:context()) -> cb_context:context(). on_successful_validation(Context) -> - ContextDoc = cb_context:doc(Context), + Setters = [fun account_is_enabled/1 + ,fun account_is_in_good_standing/1 + ,fun account_has_sms_enabled/1 + ,fun create_request/1 + ,fun validate_from/1 + ], + cb_context:validators(Context, Setters). + +-spec create_request(cb_context:context()) -> cb_context:context(). +create_request(Context) -> + Payload = cb_context:doc(Context), AccountId = cb_context:account_id(Context), AuthAccountId = cb_context:auth_account_id(Context), - MODB = cb_context:account_modb(Context), ResellerId = cb_context:reseller_id(Context), - Realm = kzd_accounts:fetch_realm(AccountId), {AuthorizationType, Authorization, OwnerId} = case {cb_context:user_id(Context), cb_context:auth_user_id(Context)} of @@ -168,68 +176,108 @@ on_successful_validation(Context) -> {<<"user">>, UserId, UserId} end, - {ToNum, ToOptions} = build_number(kz_json:get_value(<<"to">>, ContextDoc)), - ToUser = - case kapps_account_config:get_global(AccountId, ?MOD_CONFIG_CAT, <<"api_e164_convert_to">>, 'false') - andalso knm_converters:is_reconcilable(filter_number(ToNum), AccountId) - of - 'true' -> knm_converters:normalize(filter_number(ToNum), AccountId); - 'false' -> ToNum - end, + To = kz_json:get_value(<<"to">>, Payload), + {Type, ToNum} = case knm_converters:is_reconcilable(To) of + 'true' -> {<<"offnet">>, knm_converters:normalize(To, AccountId)}; + 'false' -> {<<"onnet">>, To} + end, + + FromNum = kz_json:get_value(<<"from">>, Payload, get_default_caller_id(Context, Type, OwnerId)), + + CCVs = [{<<"Account-ID">>, AccountId} + ,{<<"Reseller-ID">>, ResellerId} + ,{<<"Authorizing-Type">>, AuthorizationType} + ,{<<"Authorizing-ID">>, Authorization} + ,{<<"Owner-ID">>, OwnerId} + ], + + KVs = [{<<"Message-ID">>, cb_context:req_id(Context)} + ,{<<"From">>, FromNum} + ,{<<"To">>, ToNum} + ,{<<"Body">>, kz_json:get_value(<<"body">>, Payload)} + ,{<<"Account-ID">>, cb_context:account_id(Context)} + ,{<<"Custom-Channel-Vars">>, kz_json:from_list(CCVs)} + ,{<<"Route-Type">>, Type} + ,{<<"from">>, null} + ,{<<"to">>, null} + ,{<<"body">>, null} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ], + + cb_context:set_doc(Context, kz_json:set_values(KVs, Payload)). + +-spec get_default_caller_id(cb_context:context(), binary(), kz_term:api_binary()) -> kz_term:api_binary(). +get_default_caller_id(Context, <<"offnet">>, 'undefined') -> + {'ok', EP} = kz_endpoint:get(cb_context:account_id(Context), cb_context:account_id(Context)), + kzd_caller_id:external(kzd_accounts:caller_id(EP, kz_json:new())); +get_default_caller_id(Context, <<"onnet">>, 'undefined') -> + {'ok', EP} = kz_endpoint:get(cb_context:account_id(Context), cb_context:account_id(Context)), + kzd_caller_id:internal(kzd_accounts:caller_id(EP, kz_json:new())); +get_default_caller_id(Context, <<"offnet">>, OwnerId) -> + {'ok', EP} = kz_endpoint:get(OwnerId, cb_context:account_id(Context)), + kzd_caller_id:external(kzd_accounts:caller_id(EP, kz_json:new())); +get_default_caller_id(Context, <<"onnet">>, OwnerId) -> + {'ok', EP} = kz_endpoint:get(OwnerId, cb_context:account_id(Context)), + kzd_caller_id:internal(kzd_accounts:caller_id(EP, kz_json:new())). + + +-spec account_is_enabled(cb_context:context()) -> cb_context:context(). +account_is_enabled(Context) -> + case kzd_accounts:enabled(cb_context:account_doc(Context)) of + 'true' -> Context; + 'false' -> cb_context:add_system_error('disabled', Context) + end. - {FromNum, FromOptions} = build_number(kz_json:get_value(<<"from">>, ContextDoc, get_default_caller_id(Context, OwnerId))), - FromUser = - case kapps_account_config:get_global(AccountId, ?MOD_CONFIG_CAT, <<"api_e164_convert_from">>, 'false') - andalso knm_converters:is_reconcilable(filter_number(FromNum), AccountId) - of - 'true' -> knm_converters:normalize(filter_number(FromNum), AccountId); - 'false' -> FromNum - end, +-spec account_is_in_good_standing(cb_context:context()) -> cb_context:context(). +account_is_in_good_standing(Context) -> + case kz_services_standing:acceptable(cb_context:account_id(Context)) of + {'true', _} -> Context; + {'false', #{message := Msg}} -> cb_context:add_system_error('account', Msg, Context) + end. + +-spec account_has_sms_enabled(cb_context:context()) -> cb_context:context(). +account_has_sms_enabled(Context) -> + case kz_services_im:is_sms_enabled(cb_context:account_id(Context)) of + 'true' -> Context; + 'false' -> cb_context:add_system_error('account', <<"sms services not enabled for account">>, Context) + end. - AddrOpts = [{<<"SMPP-Address-From-", K/binary>>, V} || {K, V} <- FromOptions] - ++ [{<<"SMPP-Address-To-", K/binary>>, V} || {K, V} <- ToOptions], - - JObj = kz_json:from_list( - [{<<"_id">>, kazoo_modb_util:modb_id()} - ,{<<"request">>, <>} - ,{<<"request_user">>, ToUser} - ,{<<"request_realm">>, Realm} - ,{<<"to">>, <>} - ,{<<"to_user">>, ToUser} - ,{<<"to_realm">>, Realm} - ,{<<"from">>, <>} - ,{<<"from_user">>, FromUser} - ,{<<"from_realm">>, Realm} - ,{<<"pvt_status">>, <<"queued">>} - ,{<<"pvt_reseller_id">>, ResellerId} - ,{<<"pvt_owner_id">>, OwnerId} - ,{<<"pvt_authorization_type">>, AuthorizationType} - ,{<<"pvt_authorization">>, Authorization} - ,{<<"pvt_origin">>, <<"api">>} - ,{<<"pvt_address_options">>, kz_json:from_list(AddrOpts)} - ] - ), - Doc = kz_doc:update_pvt_parameters(kz_json:merge(ContextDoc, JObj), MODB, [{'type', <<"sms">>}]), - cb_context:set_doc(cb_context:set_account_db(Context, MODB), Doc). - --define(CALLER_ID_INTERNAL, [<<"caller_id">>, <<"internal">>, <<"number">>]). --define(CALLER_ID_EXTERNAL, [<<"caller_id">>, <<"external">>, <<"number">>]). - --spec get_default_caller_id(cb_context:context(), kz_term:api_binary()) -> kz_term:api_binary(). -get_default_caller_id(Context, 'undefined') -> - {'ok', JObj} = kzd_accounts:fetch(cb_context:account_id(Context)), - kz_json:get_first_defined([?CALLER_ID_INTERNAL, ?CALLER_ID_EXTERNAL] - ,JObj - ,kz_privacy:anonymous_caller_id_number(cb_context:account_id(Context)) - ); -get_default_caller_id(Context, OwnerId) -> - AccountDb = cb_context:account_db(Context), - {'ok', JObj1} = kzd_accounts:fetch(AccountDb), - {'ok', JObj2} = kz_datamgr:open_cache_doc(AccountDb, OwnerId), - kz_json:get_first_defined([?CALLER_ID_INTERNAL, ?CALLER_ID_EXTERNAL] - ,kz_json:merge(JObj1, JObj2) - ,kz_privacy:anonymous_caller_id_number(cb_context:account_id(Context)) - ). +-spec validate_from(cb_context:context()) -> cb_context:context(). +validate_from(Context) -> + case kz_api_sms:route_type(cb_context:doc(Context)) of + <<"onnet">> -> + Context; + <<"offnet">> -> + Setters = [{fun number_exists/2, kz_api_sms:from(cb_context:doc(Context))} + ,fun number_belongs_to_account/1 + ,fun number_has_sms_enabled/1 + ], + cb_context:validators(Context, Setters) + end. + +-spec number_exists(cb_context:context(), kz_term:ne_binary()) -> cb_context:context(). +number_exists(Context, Number) -> + case knm_phone_number:fetch(Number) of + {'error', _R} -> cb_context:add_validation_error(<<"from">>, <<"invalid">>, <<"number is invalid">>, Context); + {'ok', KNumber} -> cb_context:store(Context, 'from_number', KNumber) + end. + +-spec number_belongs_to_account(cb_context:context()) -> cb_context:context(). +number_belongs_to_account(Context) -> + Number = cb_context:fetch(Context, 'from_number'), + AccountId = cb_context:account_id(Context), + case knm_phone_number:assigned_to(Number) =:= AccountId of + 'true' -> Context; + 'false' -> cb_context:add_validation_error(<<"from">>, <<"forbidden">>, <<"number does not belong to account">>, Context) + end. + +-spec number_has_sms_enabled(cb_context:context()) -> cb_context:context(). +number_has_sms_enabled(Context) -> + Number = cb_context:fetch(Context, 'from_number'), + case knm_sms:enabled(Number) of + 'true' -> Context; + 'false' -> cb_context:add_validation_error(<<"from">>, <<"forbidden">>, <<"number does not have sms enabled">>, Context) + end. %%------------------------------------------------------------------------------ %% @doc Attempt to load a summarized listing of all instances of this @@ -271,29 +319,3 @@ normalize_view_results(JObj, Acc) -> Date = kz_time:rfc1036(kz_json:get_value(<<"created">>, ValueJObj)), [kz_json:set_value(<<"date">>, Date, JObj) | Acc]. --spec filter_number(binary()) -> binary(). -filter_number(Number) -> - << <> || <> <= Number, is_digit(X)>>. - --spec is_digit(integer()) -> boolean(). -is_digit(N) when is_integer(N), - N >= $0, - N =< $9 -> true; -is_digit(_) -> false. - --spec build_number(kz_term:ne_binary()) -> {kz_term:api_binary(), kz_term:proplist()}. -build_number(Number) -> - N = binary:split(Number, <<",">>, ['global']), - case N of - [_One] -> {Number, []}; - _ -> lists:foldl(fun parse_number/2, {'undefined', []}, N) - end. - --spec parse_number(kz_term:ne_binary(), {kz_term:api_binary(), kz_term:proplist()}) -> - {kz_term:api_binary(), kz_term:proplist()}. -parse_number(<<"TON=", N/binary>>, {Num, Options}) -> - {Num, [{<<"TON">>, kz_term:to_integer(N) } | Options]}; -parse_number(<<"NPI=", N/binary>>, {Num, Options}) -> - {Num, [{<<"NPI">>, kz_term:to_integer(N) } | Options]}; -parse_number(N, {_, Options}) -> - {N, Options}. diff --git a/applications/doodle/src/doodle.app.src b/applications/doodle/src/doodle.app.src index 74a8e2b1084..0817203d477 100644 --- a/applications/doodle/src/doodle.app.src +++ b/applications/doodle/src/doodle.app.src @@ -1,14 +1,12 @@ {application,doodle, - [{applications,[callflow,kazoo,kazoo_amqp,kazoo_apps, - kazoo_bindings,kazoo_caches,kazoo_call, + [{applications,[kazoo,kazoo_amqp,kazoo_apps,kazoo_caches, kazoo_data,kazoo_documents,kazoo_endpoint, - kazoo_modb,kazoo_number_manager,kazoo_stdlib, - kazoo_token_buckets,kernel,lager,stdlib]}, + kazoo_im,kazoo_number_manager,kazoo_stdlib,kernel,lager, + stdlib]}, {description,"doodle - sms store and forward"}, {env,[{is_kazoo_app,true}]}, {mod,{doodle_app,[]}}, {modules,[]}, - {registered,[doodle_cache,doodle_shared_listener,doodle_sup, - doodle_exe_sup,doodle_event_handler_sup, - doodle_inbound_listener_sup]}, + {registered,[doodle_cache,doodle_listener_sup,doodle_sup, + tf_exe_listener,tf_exe_sup]}, {vsn,"4.0.0"}]}. diff --git a/applications/doodle/src/doodle.hrl b/applications/doodle/src/doodle.hrl index 2f106f6313e..1747e785c53 100644 --- a/applications/doodle/src/doodle.hrl +++ b/applications/doodle/src/doodle.hrl @@ -4,7 +4,8 @@ -include_lib("kazoo_stdlib/include/kz_databases.hrl"). -include_lib("kazoo/include/kz_api_literals.hrl"). -include_lib("kazoo_number_manager/include/knm_phone_number.hrl"). --include_lib("kazoo_call/include/kapps_call_command_types.hrl"). +-include_lib("kazoo_im/include/kapps_im_command_types.hrl"). +-include_lib("kazoo_amqp/include/kz_amqp.hrl"). -define(APP_NAME, <<"doodle">>). -define(APP_VERSION, <<"4.0.0">>). @@ -52,5 +53,18 @@ -define(OUTBOUND_POOL_ARG(K),[<<"default">>, <<"outbound">>, <<"pool">>, K]). -define(OUTBOUND_EXCHANGE_ARG(K),[<<"default">>, <<"outbound">>, <<"pool">>, <<"exchange">>, K]). + +-ifdef(OTP_RELEASE). +%% >= OTP 21 +-define(CATCH(Type, Reason, Stacktrace), Type:Reason:Stacktrace). +-define(LOGSTACK(Stacktrace), kz_log:log_stacktrace(Stacktrace)). +-else. +%% =< OTP 20 +-define(CATCH(Type, Reason, Stacktrace), Type:Reason). +-define(LOGSTACK(Stacktrace), kz_util:log_stacktrace()). +-endif. + +-define(DEFAULT_CHILD_KEY, <<"_">>). + -define(DOODLE_HRL, 'true'). -endif. diff --git a/applications/doodle/src/doodle_api.erl b/applications/doodle/src/doodle_api.erl deleted file mode 100644 index 538779c9d00..00000000000 --- a/applications/doodle/src/doodle_api.erl +++ /dev/null @@ -1,84 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @copyright (C) 2013-2019, 2600Hz -%%% @doc Handle sms api docs -%%% @end -%%%----------------------------------------------------------------------------- --module(doodle_api). - --export([handle_api_sms/2]). - --include("doodle.hrl"). - --spec handle_api_sms(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -handle_api_sms(Db, Id) -> - {'ok', Doc} = kz_datamgr:open_doc(Db, Id), - Status = kz_json:get_value(<<"pvt_status">>, Doc), - Origin = kz_json:get_value(<<"pvt_origin">>, Doc), - FetchId = kz_binary:rand_hex(16), - maybe_handle_sms_document(Status, Origin, FetchId, Id, Doc). - --spec maybe_handle_sms_document(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object()) -> 'ok'. -maybe_handle_sms_document(<<"queued">>, <<"api">>, FetchId, Id, JObj) -> - process_sms_api_document(FetchId, Id, JObj); -maybe_handle_sms_document(_Status, _Origin, _FetchId, _Id, _JObj) -> 'ok'. - --spec process_sms_api_document(kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object()) -> 'ok'. -process_sms_api_document(FetchId, <<_:7/binary, CallId/binary>> = _Id, APIJObj) -> - ReqResp = kz_amqp_worker:call(route_req(FetchId, CallId, APIJObj) - ,fun kapi_route:publish_req/1 - ,fun kapi_route:is_actionable_resp/1 - ), - case ReqResp of - {'error', _R} -> - lager:info("did not receive route response for request ~s: ~p", [FetchId, _R]); - {'ok', RespJObj} -> - 'true' = kapi_route:resp_v(RespJObj), - send_route_win(FetchId, CallId, RespJObj) - end. - --spec send_route_win(kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object()) -> 'ok'. -send_route_win(FetchId, CallId, JObj) -> - ServerQ = kz_json:get_value(<<"Server-ID">>, JObj), - CCVs = kz_json:get_value(<<"Custom-Channel-Vars">>, JObj, kz_json:new()), - Win = [{<<"Msg-ID">>, FetchId} - ,{<<"Call-ID">>, CallId} - ,{<<"Control-Queue">>, <<"chatplan_ignored">>} - ,{<<"Custom-Channel-Vars">>, CCVs} - | kz_api:default_headers(<<"dialplan">>, <<"route_win">>, ?APP_NAME, ?APP_VERSION) - ], - lager:debug("sms api handler sending route_win to ~s", [ServerQ]), - kz_amqp_worker:cast(Win, fun(Payload) -> kapi_route:publish_win(ServerQ, Payload) end). - --spec route_req(kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object()) -> kz_term:proplist(). -route_req(FetchId, CallId, JObj) -> - [{<<"Msg-ID">>, FetchId} - ,{<<"Call-ID">>, CallId} - ,{<<"Message-ID">>, kz_json:get_value(<<"Message-ID">>, JObj, kz_binary:rand_hex(16))} - ,{<<"Caller-ID-Name">>, kz_json:get_value(<<"from_user">>, JObj)} - ,{<<"Caller-ID-Number">>, kz_json:get_value(<<"from_user">>, JObj)} - ,{<<"To">>, kz_json:get_value(<<"to">>, JObj)} - ,{<<"From">>, kz_json:get_value(<<"from">>, JObj)} - ,{<<"Request">>, kz_json:get_value(<<"request">>, JObj)} - ,{<<"Body">>, kz_json:get_value(<<"body">>, JObj)} - ,{<<"Custom-Channel-Vars">>, kz_json:from_list(route_req_ccvs(FetchId, JObj))} - ,{<<"Resource-Type">>, <<"sms">>} - ,{<<"Call-Direction">>, <<"inbound">>} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ]. - --spec route_req_ccvs(kz_term:ne_binary(), kz_json:object()) -> kz_term:proplist(). -route_req_ccvs(FetchId, JObj) -> - props:filter_undefined( - [{<<"Fetch-ID">>, FetchId} - ,{<<"Account-ID">>, kz_doc:account_id(JObj)} - ,{<<"Reseller-ID">>, kzd_services:reseller_id(JObj)} - ,{<<"Authorizing-Type">>, kz_json:get_value(<<"pvt_authorization_type">>, JObj)} - ,{<<"Authorizing-ID">>, kz_json:get_value(<<"pvt_authorization">>, JObj)} - ,{<<"Owner-ID">>, kz_json:get_value(<<"pvt_owner_id">>, JObj)} - ,{<<"Channel-Authorized">>, 'true'} - ,{<<"Doc-Revision">>, kz_doc:revision(JObj)} - ,{<<"Doc-ID">>, kz_doc:id(JObj)} - ,{<<"Scheduled-Delivery">>, kz_json:get_value(<<"scheduled">>, JObj)} - ,{<<"API-Call">>, 'true'} - | kz_json:to_proplist(<<"pvt_address_options">>, JObj) - ]). diff --git a/applications/doodle/src/doodle_app.erl b/applications/doodle/src/doodle_app.erl index 8390eb2f269..a2b323e4eb1 100644 --- a/applications/doodle/src/doodle_app.erl +++ b/applications/doodle/src/doodle_app.erl @@ -8,10 +8,9 @@ -behaviour(application). -include("doodle.hrl"). --define(ACCOUNT_CRAWLER_BINDING, <<"tasks.account_crawler">>). +-include_lib("kazoo_amqp/include/kz_amqp.hrl"). -export([start/2, stop/1]). --export([register_views/0]). %%------------------------------------------------------------------------------ %% @doc Implement the application start behaviour. @@ -22,17 +21,6 @@ start(_Type, _Args) -> _ = declare_exchanges(), register_views(), _ = kapps_maintenance:bind_and_register_views('doodle', 'doodle_app', 'register_views'), - case kapps_config:get_json(?CONFIG_CAT, <<"reschedule">>) =:= undefined - andalso kz_json:load_fixture_from_file(?APP, <<"fixtures">>, <<"reschedule.json">>) - of - false -> ok; - {'error', Err} -> lager:error("default sms is undefined and cannot read default from file: ~p", [Err]); - JObj -> kapps_config:set(?CONFIG_CAT, <<"reschedule">>, JObj) - end, - lager:debug("Start listening for tasks.account_crawler trigger"), - _ = kazoo_bindings:bind(?ACCOUNT_CRAWLER_BINDING, - 'doodle_maintenance', - 'start_check_sms_by_account'), doodle_sup:start_link(). %%------------------------------------------------------------------------------ @@ -41,20 +29,14 @@ start(_Type, _Args) -> %%------------------------------------------------------------------------------ -spec stop(any()) -> any(). stop(_State) -> - _ = kazoo_bindings:unbind(?ACCOUNT_CRAWLER_BINDING, - 'doodle_maintenance', - 'start_check_sms_by_account'), - _ = kapps_maintenance:unbind('register_views', 'doodle_app', 'register_views'), 'ok'. -spec declare_exchanges() -> 'ok'. declare_exchanges() -> - _ = kapi_notifications:declare_exchanges(), - _ = kapi_route:declare_exchanges(), _ = kapi_sms:declare_exchanges(), - _ = kapi_registration:declare_exchanges(), - kapi_self:declare_exchanges(). + _ = kapi_self:declare_exchanges(). -spec register_views() -> 'ok'. register_views() -> kz_datamgr:register_views_from_folder('doodle'). + diff --git a/applications/doodle/src/doodle_delivery_handler.erl b/applications/doodle/src/doodle_delivery_handler.erl deleted file mode 100644 index b92505af3d6..00000000000 --- a/applications/doodle/src/doodle_delivery_handler.erl +++ /dev/null @@ -1,42 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @copyright (C) 2010-2019, 2600Hz -%%% @doc Handlers for various AMQP payloads -%%% @author James Aimonetti -%%% @end -%%%----------------------------------------------------------------------------- --module(doodle_delivery_handler). - --export([handle_req/2]). - --include("doodle.hrl"). - --spec handle_req(kz_json:object(), kz_term:proplist()) -> 'ok'. -handle_req(JObj, _Props) -> - 'true' = kapi_sms:delivery_v(JObj), - _ = kz_util:put_callid(JObj), - maybe_update_doc(JObj). - --spec maybe_update_doc(kz_json:object()) -> any(). -maybe_update_doc(JObj) -> - DeliveryCode = kz_json:get_value(<<"Delivery-Result-Code">>, JObj), - Status = kz_json:get_value(<<"Status">>, JObj), - Value = case {Status, DeliveryCode} of - {_ , <<"200">> } -> <<"delivered">>; - {_ , <<"202">> } -> <<"accepted">>; - {<<"Success">>, _ } -> <<"completed">>; - _Else -> <<"pending">> - end, - update_doc(JObj, Value). - --spec update_doc(kz_json:object(), kz_term:ne_binary()) -> any(). -update_doc(JObj, Value) -> - CallId = kz_json:get_value(<<"Call-ID">>, JObj), - CCVs = kz_json:get_value(<<"Custom-Channel-Vars">>, JObj), - AccountId = kz_json:get_value(<<"Account-ID">>, CCVs), - case kazoo_modb:open_doc(AccountId, CallId) of - {'ok', Doc} -> - UpdatedDoc = kz_json:set_value(<<"pvt_status">>, Value, Doc), - kazoo_modb:save_doc(AccountId, UpdatedDoc); - {'error', _E} -> - lager:debug("error reading doc ~s from modb in account ~s",[CallId, AccountId]) - end. diff --git a/applications/doodle/src/doodle_doc_handler.erl b/applications/doodle/src/doodle_doc_handler.erl deleted file mode 100644 index 57a06fc7b49..00000000000 --- a/applications/doodle/src/doodle_doc_handler.erl +++ /dev/null @@ -1,29 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @copyright (C) 2013-2019, 2600Hz -%%% @doc Handlers for various AMQP payloads -%%% @end -%%%----------------------------------------------------------------------------- --module(doodle_doc_handler). - --export([handle_req/2]). - --include("doodle.hrl"). --include_lib("kazoo_amqp/include/kapi_conf.hrl"). - --spec handle_req(kz_json:object(), kz_term:proplist()) -> 'ok'. -handle_req(JObj, _Props) -> - 'true' = kapi_conf:doc_update_v(JObj), - Id = kapi_conf:get_id(JObj), - Db = kapi_conf:get_database(JObj), - Type = kapi_conf:get_type(JObj), - Action = kz_api:event_name(JObj), - handle_doc(Action, Type, Db, Id). - --spec handle_doc(kz_term:api_binary(), kz_term:api_binary(), kz_term:api_binary(), kz_term:api_binary()) -> 'ok'. -handle_doc(?DOC_CREATED, <<"sms">>, Db, Id) -> - doodle_api:handle_api_sms(Db, Id); -handle_doc(_, <<"device">>, ?MATCH_ACCOUNT_RAW(AccountId), Id) -> - doodle_maintenance:check_sms_by_device_id(AccountId, Id); -handle_doc(_, <<"user">>, ?MATCH_ACCOUNT_RAW(AccountId), Id) -> - doodle_maintenance:check_sms_by_owner_id(AccountId, Id); -handle_doc(_, _, _, _) -> 'ok'. diff --git a/applications/doodle/src/doodle_exe.erl b/applications/doodle/src/doodle_exe.erl deleted file mode 100644 index edd8580d30c..00000000000 --- a/applications/doodle/src/doodle_exe.erl +++ /dev/null @@ -1,655 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @copyright (C) 2010-2019, 2600Hz -%%% @doc -%%% @author Karl Anderson -%%% @end -%%%----------------------------------------------------------------------------- --module(doodle_exe). --behaviour(gen_listener). - -%% API --export([start_link/1]). --export([relay_amqp/2, send_amqp/3]). --export([get_call/1, set_call/1]). --export([callid/1, callid/2]). --export([queue_name/1]). --export([control_queue/1, control_queue/2]). --export([continue/1, continue/2]). --export([branch/2]). --export([stop/1]). --export([transfer/1]). --export([control_usurped/1]). --export([get_branch_keys/1, get_all_branch_keys/1]). --export([attempt/1, attempt/2]). --export([wildcard_is_empty/1]). --export([callid_update/2]). --export([add_event_listener/2]). - -%% gen_listener callbacks --export([init/1 - ,handle_call/3 - ,handle_cast/2 - ,handle_info/2 - ,handle_event/2 - ,terminate/2 - ,code_change/3 - ]). - --include("doodle.hrl"). - --define(SERVER, ?MODULE). - --define(CALL_SANITY_CHECK, 30 * ?MILLISECONDS_IN_SECOND). - --define(RESPONDERS, [{{?MODULE, 'relay_amqp'} - ,[{<<"*">>, <<"*">>}] - } - ]). --define(QUEUE_NAME, <<>>). --define(QUEUE_OPTIONS, []). --define(CONSUME_OPTIONS, []). - --record(state, {call = kapps_call:new() :: kapps_call:call() - ,flow = kz_json:new() :: kz_json:object() - ,cf_module_pid :: {pid(), reference()} | 'undefined' - ,cf_module_old_pid :: {pid(), reference()} | 'undefined' - ,status = <<"sane">> :: kz_term:ne_binary() - ,queue :: kz_term:api_binary() - ,self = self() - }). --type state() :: #state{}. - -%%%============================================================================= -%%% API -%%%============================================================================= - -%%------------------------------------------------------------------------------ -%% @doc Starts the server. -%% @end -%%------------------------------------------------------------------------------ --spec start_link(kapps_call:call()) -> kz_types:startlink_ret(). -start_link(Call) -> - CallId = kapps_call:call_id(Call), - Bindings = [{'sms', [{'message_id', CallId} - ,{'restrict_to', ['delivery']} - ] - } - ,{'self', []} - ], - gen_listener:start_link(?SERVER, [{'responders', ?RESPONDERS} - ,{'bindings', Bindings} - ,{'queue_name', ?QUEUE_NAME} - ,{'queue_options', ?QUEUE_OPTIONS} - ,{'consume_options', ?CONSUME_OPTIONS} - ], [Call]). - --spec get_call(pid() | kapps_call:call()) -> {'ok', kapps_call:call()}. -get_call(Srv) when is_pid(Srv) -> - gen_server:call(Srv, 'get_call', ?MILLISECONDS_IN_SECOND); -get_call(Call) -> - Srv = kapps_call:kvs_fetch('consumer_pid', Call), - get_call(Srv). - --spec set_call(kapps_call:call()) -> 'ok'. -set_call(Call) -> - Srv = kapps_call:kvs_fetch('consumer_pid', Call), - gen_server:cast(Srv, {'set_call', Call}). - --spec update_call(kapps_call:call()) -> 'ok'. -update_call(Call) -> - Srv = kapps_call:kvs_fetch('consumer_pid', Call), - gen_server:cast(Srv, {'update_call', Call}). - --spec continue(kapps_call:call() | pid()) -> 'ok'. -continue(Srv) -> continue(<<"_">>, Srv). - --spec continue(kz_term:ne_binary(), kapps_call:call() | pid()) -> 'ok'. -continue(Key, Srv) when is_pid(Srv) -> - gen_listener:cast(Srv, {'continue', Key}); -continue(Key, Call) -> - update_call(Call), - Srv = kapps_call:kvs_fetch('consumer_pid', Call), - continue(Key, Srv). - --spec branch(kz_json:object(), kapps_call:call() | pid()) -> 'ok'. -branch(Flow, Srv) when is_pid(Srv) -> - gen_listener:cast(Srv, {'branch', Flow}); -branch(Flow, Call) -> - update_call(Call), - Srv = kapps_call:kvs_fetch('consumer_pid', Call), - branch(Flow, Srv). - --spec add_event_listener(pid(), {any(),any()}) -> 'ok'. -add_event_listener(Srv, {_,_}=SpawnInfo) when is_pid(Srv) -> - gen_listener:cast(Srv, {'add_event_listener', SpawnInfo}); -add_event_listener(Call, {_,_}=SpawnInfo) -> - add_event_listener(kapps_call:kvs_fetch('consumer_pid', Call), SpawnInfo). - --spec stop(kapps_call:call() | pid()) -> 'ok'. -stop(Srv) when is_pid(Srv) -> - gen_listener:cast(Srv, 'stop'); -stop(Call) -> - update_call(Call), - Srv = kapps_call:kvs_fetch('consumer_pid', Call), - stop(Srv). - --spec transfer(kapps_call:call() | pid()) -> 'ok'. -transfer(Srv) when is_pid(Srv) -> - gen_listener:cast(Srv, 'transfer'); -transfer(Call) -> - update_call(Call), - Srv = kapps_call:kvs_fetch('consumer_pid', Call), - transfer(Srv). - --spec control_usurped(kapps_call:call() | pid()) -> 'ok'. -control_usurped(Srv) when is_pid(Srv) -> - gen_listener:cast(Srv, 'control_usurped'); -control_usurped(Call) -> - update_call(Call), - Srv = kapps_call:kvs_fetch('consumer_pid', Call), - control_usurped(Srv). - --spec callid_update(kz_term:ne_binary(), kapps_call:call() | pid()) -> 'ok'. -callid_update(CallId, Srv) when is_pid(Srv) -> - gen_listener:cast(Srv, {'callid_update', CallId}); -callid_update(CallId, Call) -> - Srv = kapps_call:kvs_fetch('consumer_pid', Call), - callid_update(CallId, Srv). - - --spec callid(kapps_call:call() | pid()) -> kz_term:ne_binary(). -callid(Srv) when is_pid(Srv) -> - CallId = gen_server:call(Srv, 'callid', ?MILLISECONDS_IN_SECOND), - kz_util:put_callid(CallId), - CallId; -callid(Call) -> - Srv = kapps_call:kvs_fetch('consumer_pid', Call), - callid(Srv). - --spec callid(kz_term:api_binary(), kapps_call:call()) -> kz_term:ne_binary(). -callid(_, Call) -> - callid(Call). - --spec queue_name(kapps_call:call() | pid()) -> kz_term:ne_binary(). -queue_name(Srv) when is_pid(Srv) -> - gen_listener:queue_name(Srv); -queue_name(Call) -> - Srv = kapps_call:kvs_fetch('consumer_pid', Call), - queue_name(Srv). - - --spec control_queue(kapps_call:call() | pid()) -> kz_term:ne_binary(). -control_queue(Srv) when is_pid(Srv) -> gen_listener:call(Srv, 'control_queue_name'); -control_queue(Call) -> control_queue(kapps_call:kvs_fetch('consumer_pid', Call)). - --spec control_queue(kz_term:api_binary(), kapps_call:call() | pid()) -> kz_term:ne_binary(). -control_queue(_, Call) -> control_queue(Call). - --spec get_branch_keys(kapps_call:call() | pid()) -> {'branch_keys', kz_json:path()}. -get_branch_keys(Srv) when is_pid(Srv) -> - gen_listener:call(Srv, 'get_branch_keys'); -get_branch_keys(Call) -> - Srv = kapps_call:kvs_fetch('consumer_pid', Call), - get_branch_keys(Srv). - --spec get_all_branch_keys(kapps_call:call() | pid()) -> {'branch_keys', kz_json:path()}. -get_all_branch_keys(Srv) when is_pid(Srv) -> - gen_listener:call(Srv, {'get_branch_keys', 'all'}); -get_all_branch_keys(Call) -> - Srv = kapps_call:kvs_fetch('consumer_pid', Call), - get_all_branch_keys(Srv). - --spec attempt(kapps_call:call() | pid()) -> - {'attempt_resp', 'ok'} | - {'attempt_resp', {'error', any()}}. -attempt(Srv) -> attempt(<<"_">>, Srv). - --spec attempt(kz_json:key(), kapps_call:call() | pid()) -> - {'attempt_resp', 'ok'} | - {'attempt_resp', {'error', any()}}. -attempt(Key, Srv) when is_pid(Srv) -> - gen_listener:call(Srv, {'attempt', Key}); -attempt(Key, Call) -> - Srv = kapps_call:kvs_fetch('consumer_pid', Call), - attempt(Key, Srv). - --spec wildcard_is_empty(kapps_call:call() | pid()) -> boolean(). -wildcard_is_empty(Srv) when is_pid(Srv) -> - gen_listener:call(Srv, 'wildcard_is_empty'); -wildcard_is_empty(Call) -> - Srv = kapps_call:kvs_fetch('consumer_pid', Call), - wildcard_is_empty(Srv). - --spec relay_amqp(kz_json:object(), kz_term:proplist()) -> any(). -relay_amqp(JObj, Props) -> - Pids = case props:get_value('cf_module_pid', Props) of - P when is_pid(P) -> [P | props:get_value('cf_event_pids', Props, [])]; - _ -> props:get_value('cf_event_pids', Props, []) - end, - [kapps_call_command:relay_event(Pid, JObj) || Pid <- Pids, is_pid(Pid)]. - --spec send_amqp(pid() | kapps_call:call(), kz_term:api_terms(), kz_amqp_worker:publish_fun()) -> 'ok'. -send_amqp(Srv, API, PubFun) when is_pid(Srv), is_function(PubFun, 1) -> - gen_listener:cast(Srv, {'send_amqp', API, PubFun}); -send_amqp(Call, API, PubFun) when is_function(PubFun, 1) -> - send_amqp(kapps_call:kvs_fetch('consumer_pid', Call), API, PubFun). - -%%%============================================================================= -%%% gen_listener callbacks -%%%============================================================================= - -%%------------------------------------------------------------------------------ -%% @doc Initializes the server. -%% @end -%%------------------------------------------------------------------------------ --spec init([kapps_call:call()]) -> {'ok', state()}. -init([Call]) -> - process_flag('trap_exit', 'true'), - CallId = kapps_call:call_id(Call), - kz_util:put_callid(CallId), - gen_listener:cast(self(), 'initialize'), - {'ok', #state{call=Call}}. - -%%------------------------------------------------------------------------------ -%% @doc Handling call messages. -%% @end -%%------------------------------------------------------------------------------ --spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_types:handle_call_ret_state(state()). -handle_call('get_call', _From, #state{call=Call}=State) -> - {'reply', {'ok', Call}, State}; -handle_call('callid', _From, #state{call=Call}=State) -> - {'reply', kapps_call:call_id_direct(Call), State}; -handle_call('control_queue_name', _From, #state{call=Call}=State) -> - {'reply', kapps_call:control_queue_direct(Call), State}; -handle_call('get_branch_keys', _From, #state{flow = Flow}=State) -> - Children = kz_json:get_value(<<"children">>, Flow, kz_json:new()), - Reply = {'branch_keys', lists:delete(<<"_">>, kz_json:get_keys(Children))}, - {'reply', Reply, State}; -handle_call({'get_branch_keys', 'all'}, _From, #state{flow = Flow}=State) -> - Children = kz_json:get_value(<<"children">>, Flow, kz_json:new()), - Reply = {'branch_keys', kz_json:get_keys(Children)}, - {'reply', Reply, State}; -handle_call({'attempt', Key}, _From, #state{flow=Flow}=State) -> - case kz_json:get_value([<<"children">>, Key], Flow) of - 'undefined' -> - lager:info("attempted 'undefined' child ~s", [Key]), - Reply = {'attempt_resp', {'error', 'undefined'}}, - {'reply', Reply, State}; - NewFlow -> - case kz_json:is_empty(NewFlow) of - 'true' -> - lager:info("attempted empty child ~s", [Key]), - Reply = {'attempt_resp', {'error', 'empty'}}, - {'reply', Reply, State}; - 'false' -> - lager:info("branching to attempted child ~s", [Key]), - Reply = {'attempt_resp', 'ok'}, - {'reply', Reply, launch_cf_module(State#state{flow = NewFlow})} - end - end; -handle_call('wildcard_is_empty', _From, #state{flow = Flow}=State) -> - case kz_json:get_value([<<"children">>, <<"_">>], Flow) of - 'undefined' -> {'reply', 'true', State}; - ChildFlow -> {'reply', kz_json:is_empty(ChildFlow), State} - end; -handle_call(_Request, _From, State) -> - Reply = {'error', 'unimplemented'}, - {'reply', Reply, State}. - -%%------------------------------------------------------------------------------ -%% @doc Handling cast messages. -%% @end -%%------------------------------------------------------------------------------ --spec handle_cast(any(), state()) -> kz_types:handle_cast_ret_state(state()). -handle_cast({'set_call', Call}, State) -> - {'noreply', State#state{call=Call}}; -handle_cast({'update_call', NewCall}, #state{call=OldCall, queue=Q}=State) -> - Action = kapps_call:kvs_fetch('cf_last_action', OldCall), - Call1 = kapps_call:set_controller_queue(Q, NewCall), - Call = kapps_call:kvs_store('cf_last_action', Action, Call1), - {'noreply', State#state{call=Call}}; - -handle_cast({'continue', Key}, #state{flow=Flow - }=State) -> - lager:info("continuing to child '~s'", [Key]), - - case kz_json:get_value([<<"children">>, Key], Flow) of - 'undefined' when Key =:= <<"_">> -> - lager:info("wildcard child does not exist, we are lost...hanging up"), - stop(self()), - {'noreply', State}; - 'undefined' -> - lager:info("requested child does not exist, trying wild card ~s", [Key]), - continue(self()), - {'noreply', State}; - NewFlow -> - case kz_json:is_empty(NewFlow) of - 'false' -> {'noreply', launch_cf_module(State#state{flow=NewFlow})}; - 'true' -> - stop(self()), - {'noreply', State} - end - end; -handle_cast('stop', #state{call=Call}=State) -> - _ = kz_util:spawn(fun doodle_util:save_sms/1, [kapps_call:clear_helpers(Call)]), - {'stop', 'normal', State}; -handle_cast('transfer', State) -> - {'stop', {'shutdown', 'transfer'}, State}; -handle_cast('control_usurped', State) -> - {'stop', {'shutdown', 'control_usurped'}, State}; -handle_cast({'branch', NewFlow}, State) -> - lager:info("textflow has been branched"), - {'noreply', launch_cf_module(State#state{flow=NewFlow})}; -handle_cast({'callid_update', NewCallId}, #state{call=Call}=State) -> - kz_util:put_callid(NewCallId), - PrevCallId = kapps_call:call_id_direct(Call), - lager:info("updating callid to ~s (from ~s), catch you on the flip side", [NewCallId, PrevCallId]), - lager:info("removing call event bindings for ~s", [PrevCallId]), - gen_listener:rm_binding(self(), 'call', [{'callid', PrevCallId}]), - lager:info("binding to new call events"), - gen_listener:add_binding(self(), 'call', [{'callid', NewCallId}]), - {'noreply', State#state{call=kapps_call:set_call_id(NewCallId, Call)}}; -handle_cast({'add_event_listener', {M, A}}, #state{call=Call}=State) -> - lager:debug("trying to start evt listener ~s: ~p", [M, A]), - try doodle_event_handler_sup:new(event_listener_name(Call, M), M, [kapps_call:clear_helpers(Call) | A]) of - {'ok', P} when is_pid(P) -> - lager:debug("started event listener ~p from ~s", [P, M]), - {'noreply', State}; - _E -> - lager:debug("error starting event listener: ~p", [_E]), - {'noreply', State} - catch - _:_R -> - lager:info("failed to spawn ~s:~s: ~p", [M, _R]), - {'noreply', State} - end; -handle_cast('initialize', #state{call=Call}) -> - log_call_information(Call), - Flow = kapps_call:kvs_fetch('cf_flow', Call), - Updaters = [fun(C) -> kapps_call:kvs_store('consumer_pid', self(), C) end - ,fun(C) -> kapps_call:call_id_helper(fun callid/2, C) end - ,fun(C) -> kapps_call:control_queue_helper(fun control_queue/2, C) end - ], - CallWithHelpers = lists:foldr(fun(F, C) -> F(C) end, Call, Updaters), - {'noreply', #state{call=CallWithHelpers - ,flow=Flow - }}; -handle_cast({'gen_listener', {'created_queue', Q}}, #state{call=Call}=State) -> - {'noreply', launch_cf_module(State#state{queue=Q - ,call=kapps_call:set_controller_queue(Q, Call) - })}; -handle_cast({'send_amqp', API, PubFun}, #state{queue=Q}=State) -> - send_amqp_message(API, PubFun, Q), - {'noreply', State}; -handle_cast({'gen_listener',{'is_consuming',_IsConsuming}}, State) -> - {'noreply', State}; -handle_cast(_Msg, State) -> - lager:debug("unhandled cast: ~p", [_Msg]), - {'noreply', State}. - -event_listener_name(Call, Module) -> - <<(kapps_call:call_id_direct(Call))/binary, "-", (kz_term:to_binary(Module))/binary>>. - -%%------------------------------------------------------------------------------ -%% @doc Handling all non call/cast messages. -%% @end -%%------------------------------------------------------------------------------ --spec handle_info(any(), state()) -> kz_types:handle_info_ret_state(state()). -handle_info({'DOWN', Ref, 'process', Pid, 'normal'}, #state{cf_module_pid={Pid, Ref} - ,call=Call - }=State) -> - erlang:demonitor(Ref, ['flush']), - lager:debug("cf module ~s down normally", [kapps_call:kvs_fetch('cf_last_action', Call)]), - {'noreply', State#state{cf_module_pid='undefined'}}; -handle_info({'DOWN', Ref, 'process', Pid, _Reason}, #state{cf_module_pid={Pid, Ref} - ,call=Call - }=State) -> - erlang:demonitor(Ref, ['flush']), - LastAction = kapps_call:kvs_fetch('cf_last_action', Call), - lager:error("action ~s died unexpectedly: ~p", [LastAction, _Reason]), - continue(self()), - {'noreply', State#state{cf_module_pid='undefined'}}; -handle_info({'DOWN', _Ref, 'process', _Pid, 'normal'}, State) -> - {'noreply', State}; -handle_info({'EXIT', Pid, 'normal'}, #state{cf_module_pid={Pid, Ref} - ,call=Call - }=State) -> - erlang:demonitor(Ref, ['flush']), - lager:debug("cf module ~s down normally", [kapps_call:kvs_fetch('cf_last_action', Call)]), - {'noreply', State#state{cf_module_pid='undefined'}}; -handle_info({'EXIT', Pid, _Reason}, #state{cf_module_pid={Pid, Ref} - ,call=Call - }=State) -> - erlang:demonitor(Ref, ['flush']), - LastAction = kapps_call:kvs_fetch('cf_last_action', Call), - lager:error("action ~s died unexpectedly: ~p", [LastAction, _Reason]), - continue(self()), - {'noreply', State#state{cf_module_pid='undefined'}}; -handle_info({'EXIT', Pid, 'normal'}, #state{cf_module_old_pid={Pid, Ref} - ,call=Call - }=State) -> - erlang:demonitor(Ref, ['flush']), - lager:debug("cf module ~s down normally", [kapps_call:kvs_fetch('cf_last_action', Call)]), - {'noreply', State#state{cf_module_old_pid='undefined'}}; -handle_info({'EXIT', Pid, _Reason}, #state{cf_module_old_pid={Pid, Ref} - ,call=Call - }=State) -> - erlang:demonitor(Ref, ['flush']), - LastAction = kapps_call:kvs_fetch('cf_last_action', Call), - lager:error("action ~s died unexpectedly: ~p", [LastAction, _Reason]), - {'noreply', State#state{cf_module_old_pid='undefined'}}; -handle_info(_Msg, State) -> - lager:debug("unhandled message: ~p", [_Msg]), - {'noreply', State}. - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec handle_event(kz_json:object(), state()) -> gen_listener:handle_event_return(). -handle_event(JObj, #state{cf_module_pid=PidRef - ,call=Call - ,self=Self - }) -> - CallId = kapps_call:call_id_direct(Call), - SmsId = kapps_call:kvs_fetch(<<"sms_docid">>, Call), - Others = kapps_call:kvs_fetch('cf_event_pids', [], Call), - case {kz_util:get_event_type(JObj), kz_json:get_value(<<"Call-ID">>, JObj)} of - {{<<"call_event">>, <<"CHANNEL_TRANSFEREE">>}, _} -> - ExeFetchId = kapps_call:custom_channel_var(<<"Fetch-ID">>, Call), - TransferFetchId = kz_json:get_value([<<"Custom-Channel-Vars">>, <<"Fetch-ID">>], JObj), - _ = case ExeFetchId =:= TransferFetchId of - 'false' -> 'ok'; - 'true' -> transfer(Call) - end, - 'ignore'; - {{<<"call_event">>, <<"CHANNEL_REPLACED">>}, _} -> - ExeFetchId = kapps_call:custom_channel_var(<<"Fetch-ID">>, Call), - TransferFetchId = kz_json:get_value([<<"Custom-Channel-Vars">>, <<"Fetch-ID">>], JObj), - case ExeFetchId =:= TransferFetchId of - 'false' -> 'ignore'; - 'true' -> - ReplacedBy = kz_json:get_value(<<"Replaced-By">>, JObj), - callid_update(ReplacedBy, Call), - {'reply', [{'cf_module_pid', get_pid(PidRef)} - ,{'cf_event_pids', Others} - ]} - end; - {{<<"call_event">>, <<"usurp_control">>}, CallId} -> - _ = case kapps_call:custom_channel_var(<<"Fetch-ID">>, Call) - =:= kz_json:get_value(<<"Fetch-ID">>, JObj) - of - 'false' -> control_usurped(Self); - 'true' -> 'ok' - end, - 'ignore'; - {{<<"error">>, _}, _} -> - case kz_json:get_value([<<"Request">>, <<"Call-ID">>], JObj) of - CallId -> {'reply', [{'cf_module_pid', get_pid(PidRef)} - ,{'cf_event_pids', Others} - ]}; - 'undefined' -> {'reply', [{'cf_module_pid', get_pid(PidRef)} - ,{'cf_event_pids', Others} - ]}; - _Else -> 'ignore' - end; - {_, CallId} -> - {'reply', [{'cf_module_pid', get_pid(PidRef)} - ,{'cf_event_pids', Others} - ]}; - {_, SmsId} -> - {'reply', [{'cf_module_pid', get_pid(PidRef)} - ,{'cf_event_pids', Others} - ]}; - {{_Cat, _Name}, _Else} when Others =:= [] -> - lager:info("received ~s (~s) from call ~s while relaying for ~s" - , [_Cat, _Name, _Else, CallId]), - 'ignore'; - {_Evt, _Else} -> - lager:info("the others want to know about ~p", [_Evt]), - {'reply', [{'cf_event_pids', Others}]} - end. - --spec get_pid({pid(), any()}) -> pid(). -get_pid({Pid, _}) when is_pid(Pid) -> Pid; -get_pid(_) -> 'undefined'. - -%%------------------------------------------------------------------------------ -%% @doc This function is called by a `gen_listener' when it is about to -%% terminate. It should be the opposite of `Module:init/1' and do any -%% necessary cleaning up. When it returns, the `gen_listener' terminates -%% with Reason. The return value is ignored. -%% -%% @end -%%------------------------------------------------------------------------------ --spec terminate(any(), state()) -> 'ok'. -terminate(_Reason, #state{cf_module_pid='undefined' - }) -> - lager:info("textflow execution has been stopped: ~p", [_Reason]); -terminate(_Reason, #state{cf_module_pid={Pid, _} - }) -> - exit(Pid, 'kill'), - lager:info("textflow execution has been stopped: ~p", [_Reason]). - -%%------------------------------------------------------------------------------ -%% @doc Convert process state when code is changed. -%% @end -%%------------------------------------------------------------------------------ --spec code_change(any(), state(), any()) -> {'ok', state()}. -code_change(_OldVsn, State, _Extra) -> - {'ok', State}. - -%%%============================================================================= -%%% Internal functions -%%%============================================================================= - -%%------------------------------------------------------------------------------ -%% this function determines if the callflow module specified at the -%% current node is 'available' and attempts to launch it if so. -%% Otherwise it will advance to the next child in the flow -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec launch_cf_module(state()) -> state(). -launch_cf_module(#state{call=Call - ,flow=Flow - ,cf_module_pid=OldPidRef - }=State) -> - Module = <<(cf_module_prefix(Call))/binary, (kz_json:get_value(<<"module">>, Flow))/binary>>, - Data = kz_json:get_value(<<"data">>, Flow, kz_json:new()), - {PidRef, Action} = maybe_start_cf_module(Module, Data, Call), - _ = cf_link(PidRef), - State#state{cf_module_pid=PidRef - ,cf_module_old_pid=OldPidRef - ,call=kapps_call:kvs_store('cf_last_action', Action, Call) - }. - --spec cf_link('undefined' | kz_term:pid_ref()) -> 'true'. -cf_link('undefined') -> 'true'; -cf_link(PidRef) -> - link(get_pid(PidRef)). - --spec cf_module_prefix(kapps_call:call()) -> kz_term:ne_binary(). -cf_module_prefix(Call) -> - cf_module_prefix(Call, kapps_call:resource_type(Call)). - --spec cf_module_prefix(kapps_call:call(), kz_term:ne_binary()) -> kz_term:ne_binary(). -cf_module_prefix(_Call, <<"sms">>) -> <<"cf_sms_">>; -cf_module_prefix(_Call, _) -> <<"cf_">>. - --spec maybe_start_cf_module(kz_term:ne_binary(), kz_term:proplist(), kapps_call:call()) -> - {kz_term:pid_ref() | 'undefined', atom()}. -maybe_start_cf_module(ModuleBin, Data, Call) -> - CFModule = kz_term:to_atom(ModuleBin, 'true'), - case kz_module:is_exported(CFModule, 'handle', 2) of - 'true' -> - lager:info("moving to action '~s'", [CFModule]), - spawn_cf_module(CFModule, Data, Call); - 'false' -> - cf_module_skip(ModuleBin, Call) - end. - --spec cf_module_skip(CFModule, kapps_call:call()) -> - {'undefined', CFModule}. -cf_module_skip(CFModule, _Call) -> - lager:info("unknown textflow action '~s', skipping to next action", [CFModule]), - continue(self()), - {'undefined', CFModule}. - -%%------------------------------------------------------------------------------ -%% @doc helper function to spawn a linked callflow module, from the entry -%% point 'handle' having set the callid on the new process first -%% @end -%%------------------------------------------------------------------------------ --spec spawn_cf_module(CFModule, list(), kapps_call:call()) -> - {kz_term:pid_ref(), CFModule}. -spawn_cf_module(CFModule, Data, Call) -> - AMQPConsumer = kz_amqp_channel:consumer_pid(), - {kz_util:spawn_monitor(fun cf_module_task/4, [CFModule, Data, Call, AMQPConsumer]) - ,CFModule - }. - --spec cf_module_task(atom(), list(), kapps_call:call(), pid()) -> any(). -cf_module_task(CFModule, Data, Call, AMQPConsumer) -> - _ = kz_amqp_channel:consumer_pid(AMQPConsumer), - kz_util:put_callid(kapps_call:call_id_direct(Call)), - try CFModule:handle(Data, Call) of - _ -> 'ok' - catch - _E:R -> - ST = erlang:get_stacktrace(), - lager:info("action ~s died unexpectedly (~s): ~p", [CFModule, _E, R]), - kz_util:log_stacktrace(ST), - throw(R) - end. - -%%------------------------------------------------------------------------------ -%% @doc unlike the kapps_call_command this send command does not call the -%% functions of this module to form the headers, nor does it set -%% the reply queue. Used when this module is terminating to send -%% a hangup command without relying on the (now terminated) doodle_exe. -%% @end -%%------------------------------------------------------------------------------ --spec send_amqp_message(kz_term:api_terms(), kz_amqp_worker:publish_fun(), kz_term:ne_binary()) -> 'ok'. -send_amqp_message(API, PubFun, Q) -> - PubFun(add_server_id(API, Q)). - --spec add_server_id(kz_term:api_terms(), kz_term:ne_binary()) -> kz_term:api_terms(). -add_server_id(API, Q) when is_list(API) -> - [{<<"Server-ID">>, Q} | props:delete(<<"Server-ID">>, API)]; -add_server_id(API, Q) -> - kz_json:set_value(<<"Server-ID">>, Q, API). - --spec log_call_information(kapps_call:call()) -> 'ok'. -log_call_information(Call) -> - lager:info("executing flow ~s", [kapps_call:kvs_fetch('cf_flow_id', Call)]), - lager:info("account id ~s", [kapps_call:account_id(Call)]), - lager:info("request ~s", [kapps_call:request(Call)]), - lager:info("to ~s", [kapps_call:to(Call)]), - lager:info("from ~s", [kapps_call:from(Call)]), - lager:info("CID ~s ~s", [kapps_call:caller_id_name(Call), kapps_call:caller_id_number(Call)]), - case kapps_call:inception(Call) of - 'undefined' -> lager:info("inception on-net: using attributes for an internal call", []); - _Else -> lager:info("inception ~s: using attributes for an external call", [_Else]) - end, - lager:info("authorizing id ~s", [kapps_call:authorizing_id(Call)]). diff --git a/applications/doodle/src/doodle_inbound_handler.erl b/applications/doodle/src/doodle_inbound_handler.erl deleted file mode 100644 index aa403d93a33..00000000000 --- a/applications/doodle/src/doodle_inbound_handler.erl +++ /dev/null @@ -1,219 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @copyright (C) 2010-2019, 2600Hz -%%% @doc Handler for sms inbound AMQP payload -%%% @author Luis Azedo -%%% @end -%%%----------------------------------------------------------------------------- --module(doodle_inbound_handler). - --export([handle_req/3]). - --include("doodle.hrl"). - --spec handle_req(kz_json:object(), kz_term:proplist(), gen_listener:basic_deliver()) -> 'ok'. -handle_req(JObj, Props, Deliver) -> - Srv = props:get_value('server', Props), - case kapi_sms:inbound_v(JObj) of - 'true' -> - handle_inbound_sms(JObj, Srv, Deliver); - 'false' -> - lager:debug("error validating inbound message : ~p", [JObj]), - gen_listener:ack(Srv, Deliver) - end. - --spec handle_inbound_sms(kz_json:object(), pid(), gen_listener:basic_deliver()) -> 'ok'. -handle_inbound_sms(JObj, Srv, Deliver) -> - case maybe_relay_request(JObj) of - 'ack' -> gen_listener:ack(Srv, Deliver); - 'nack' -> gen_listener:nack(Srv, Deliver) - end. - --spec maybe_relay_request(kz_json:object()) -> 'ack' | 'nack'. -maybe_relay_request(JObj) -> - {Number, Inception} = doodle_util:get_inbound_destination(JObj), - Map = #{number => Number - ,inception => Inception - ,request => JObj - }, - Routines = [fun custom_header_token/1 - ,fun lookup_number/1 - ,fun account_from_number/1 - ,fun set_inception/1 - ,fun lookup_mdn/1 - ,fun set_static/1 - ,fun delete_headers/1 - ,fun set_realm/1 - ], - case kz_maps:exec(Routines, Map) of - #{account_id := _AccountId, used_by := <<"callflow">>} = M -> - lager:info("processing inbound sms request ~s in account ~s", [Number, _AccountId]), - process_sms_req(M); - #{account_id := _AccountId, used_by := _UsedBy} -> - lager:info("inbound sms request ~s in account ~s handled by ~s", [Number, _AccountId, _UsedBy]), - 'ack'; - M -> - lager:info("unable to determine account for ~s => ~p", [Number, M]), - %% TODO send system notify ? - 'ack' - end. - --spec process_sms_req(map()) -> 'ack' | 'nack'. -process_sms_req(#{fetch_id := FetchId, call_id := CallId, request := JObj}) -> - Req = kz_json:set_values([{<<"Msg-ID">>, FetchId} - ,{<<"Call-ID">>, CallId} - ,{<<"Channel-Authorized">>, 'true'} - ,{?CCV(<<"Fetch-ID">>), FetchId} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ], JObj), - - ReqResp = kz_amqp_worker:call(Req - ,fun kapi_route:publish_req/1 - ,fun kapi_route:is_actionable_resp/1 - ), - case ReqResp of - {'error', _R} -> - lager:info("did not receive route response for request ~s: ~p", [FetchId, _R]), - 'nack'; - {'ok', RespJObj} -> - 'true' = kapi_route:resp_v(RespJObj), - send_route_win(FetchId, CallId, RespJObj) - end. - --spec send_route_win(kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object()) -> 'ack'. -send_route_win(FetchId, CallId, JObj) -> - ServerQ = kz_json:get_value(<<"Server-ID">>, JObj), - CCVs = kz_json:get_value(<<"Custom-Channel-Vars">>, JObj, kz_json:new()), - Win = [{<<"Msg-ID">>, FetchId} - ,{<<"Call-ID">>, CallId} - ,{<<"Control-Queue">>, <<"chatplan_ignored">>} - ,{<<"Custom-Channel-Vars">>, CCVs} - | kz_api:default_headers(<<"dialplan">>, <<"route_win">>, ?APP_NAME, ?APP_VERSION) - ], - lager:debug("sms inbound handler sending route_win to ~s", [ServerQ]), - _ = kz_amqp_worker:cast(Win, fun(Payload) -> kapi_route:publish_win(ServerQ, Payload) end), - 'ack'. - -custom_header_token(#{request := JObj} = Map) -> - case kz_json:get_ne_binary_value([<<"Custom-SIP-Headers">>, <<"X-AUTH-Token">>], JObj) of - 'undefined' -> Map; - Token -> - custom_header_token(Map, JObj, Token) - end. - -custom_header_token(Map, JObj, Token) -> - case binary:split(Token, <<"@">>, ['global']) of - [AuthorizingId, AccountId | _] -> - AccountRealm = kzd_accounts:fetch_realm(AccountId), - AccountDb = kz_util:format_account_db(AccountId), - case kz_datamgr:open_cache_doc(AccountDb, AuthorizingId) of - {'ok', Doc} -> - Props = props:filter_undefined([{?CCV(<<"Authorizing-Type">>), kz_doc:type(Doc)} - ,{?CCV(<<"Authorizing-ID">>), AuthorizingId} - ,{?CCV(<<"Owner-ID">>), kzd_devices:owner_id(Doc)} - ,{?CCV(<<"Account-ID">>), AccountId} - ,{?CCV(<<"Account-Realm">>), AccountRealm} - ]), - Map#{authorizing_id => AuthorizingId - ,account_id => AccountId - ,request => kz_json:set_values(Props, JObj) - }; - _Else -> - lager:warning("unexpected result reading doc ~s/~s => ~p", [AuthorizingId, AccountId, _Else]), - Map - end; - _Else -> - lager:warning("unexpected result spliting Token => ~p", [_Else]), - Map - end. - - - - -lookup_number(#{account_id := _AccountId} = Map) -> Map; -lookup_number(#{number := Number} = Map) -> - case knm_phone_number:fetch(Number) of - {'error', _R} -> - lager:info("unable to determine account for ~s: ~p", [Number, _R]), - Map; - {'ok', KNumber} -> - Map#{phone_number => KNumber, used_by => knm_phone_number:used_by(KNumber)} - end; -lookup_number(Map) -> - Map. - -account_from_number(#{account_id := _AccountId} = Map) -> Map; -account_from_number(#{phone_number := KNumber, request := JObj} = Map) -> - case knm_phone_number:assigned_to(KNumber) of - 'undefined' -> Map; - AccountId -> - AccountRealm = kzd_accounts:fetch_realm(AccountId), - Props = [{?CCV(<<"Account-ID">>), AccountId} - ,{?CCV(<<"Account-Realm">>), AccountRealm} - ,{?CCV(<<"Authorizing-Type">>), <<"resource">>} - ], - Map#{account_id => AccountId - ,request => kz_json:set_values(Props, JObj) - } - end; -account_from_number(Map) -> - Map. - --spec set_inception(map()) -> map(). -set_inception(#{inception := <<"off-net">>, request := JObj} = Map) -> - Request = kz_json:get_value(<<"From">>, JObj), - Map#{request => kz_json:set_value(?CCV(<<"Inception">>), Request, JObj)}; -set_inception(#{request := JObj} = Map) -> - Map#{request => kz_json:delete_keys([<<"Inception">>, ?CCV(<<"Inception">>)], JObj)}. - --spec lookup_mdn(map()) -> map(). -lookup_mdn(#{authorizing_id := _AuthorizingId} = Map) -> Map; -lookup_mdn(#{phone_number := KNumber, request := JObj} = Map) -> - Number = knm_phone_number:number(KNumber), - case doodle_util:lookup_mdn(Number) of - {'ok', Id, OwnerId} -> - Props = props:filter_undefined([{?CCV(<<"Authorizing-Type">>), <<"device">>} - ,{?CCV(<<"Authorizing-ID">>), Id} - ,{?CCV(<<"Owner-ID">>), OwnerId} - ]), - JObj1 = kz_json:delete_keys([?CCV(<<"Authorizing-Type">>) - ,?CCV(<<"Authorizing-ID">>) - ], JObj), - Map#{request => kz_json:set_values(Props, JObj1)}; - {'error', _} -> Map - end; -lookup_mdn(Map) -> Map. - --spec set_static(map()) -> map(). -set_static(#{request := JObj} = Map) -> - FetchId = kz_api:msg_id(JObj, kz_binary:rand_hex(16)), - CallId = kz_api:call_id(JObj, kz_binary:rand_hex(16)), - Props = [{<<"Resource-Type">>, <<"sms">>} - ,{<<"Call-Direction">>, <<"inbound">>} - ,{?CCV(<<"Channel-Authorized">>), 'true'} - ], - Map#{fetch_id => FetchId - ,call_id => CallId - ,request => kz_json:set_values(Props, JObj) - }. - --spec delete_headers(map()) -> map(). -delete_headers(#{request := JObj} = Map) -> - Map#{request => kz_api:remove_defaults(JObj)}. - --spec set_realm(map()) -> map(). -set_realm(#{request:= JObj, account_id := _AccountId} = Map) -> - Realm = kz_json:get_value(?CCV(<<"Account-Realm">>), JObj), - Keys = [<<"To">>, <<"From">>, {<<"To">>, <<"Request">>}], - KVs = lists:foldl(fun({K1, K2}, Acc) -> - V = kz_json:get_value(K1, JObj), - [ set_realm_value(K2, V, Realm) | Acc]; - (K, Acc) -> - V = kz_json:get_value(K, JObj), - [ set_realm_value(K, V, Realm) | Acc] - end, [], Keys), - Map#{request => kz_json:set_values(KVs, JObj)}; -set_realm(Map) -> Map. - --spec set_realm_value(K, kz_term:ne_binary(), kz_term:ne_binary()) -> {K, kz_term:ne_binary()}. -set_realm_value(K, Value, Realm) -> - {K, <>}. diff --git a/applications/doodle/src/doodle_inbound_listener.erl b/applications/doodle/src/doodle_inbound_listener.erl deleted file mode 100644 index 4059069ffd3..00000000000 --- a/applications/doodle/src/doodle_inbound_listener.erl +++ /dev/null @@ -1,165 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @copyright (C) 2013-2019, 2600Hz -%%% @doc -%%% @end -%%%----------------------------------------------------------------------------- --module(doodle_inbound_listener). --behaviour(gen_listener). - --export([start_link/1]). --export([init/1 - ,handle_call/3 - ,handle_cast/2 - ,handle_info/2 - ,handle_event/2 - ,terminate/2 - ,code_change/3 - ,format_status/2 - ,handle_debug/3 - ]). - --include("doodle.hrl"). - --define(SERVER, ?MODULE). - --record(state, {connection :: amqp_listener_connection() - }). --type state() :: #state{}. - --define(BINDINGS(Ex), [{'sms', [{'exchange', Ex} - ,{'restrict_to', ['inbound']} - ]} - ,{'self', []} - ]). --define(RESPONDERS, [{'doodle_inbound_handler' - ,[{<<"message">>, <<"inbound">>}] - } - ]). - --define(QUEUE_OPTIONS, [{'exclusive', 'false'} - ,{'durable', 'true'} - ,{'auto_delete', 'false'} - ,{'arguments', [{<<"x-message-ttl">>, 'infinity'} - ,{<<"x-max-length">>, 'infinity'} - ]} - ]). --define(CONSUME_OPTIONS, [{'exclusive', 'false'} - ,{'no_ack', 'false'} - ]). - -%%%============================================================================= -%%% API -%%%============================================================================= - -%%------------------------------------------------------------------------------ -%% @doc Starts the server. -%% @end -%%------------------------------------------------------------------------------ --spec start_link(amqp_listener_connection()) -> kz_types:startlink_ret(). -start_link(#amqp_listener_connection{broker=Broker - ,exchange=Exchange - ,type=Type - ,queue=Queue - ,options=Options - }=C) -> - Exchanges = [{Exchange, Type, Options}], - gen_listener:start_link(?SERVER - ,[{'bindings', ?BINDINGS(Exchange)} - ,{'responders', ?RESPONDERS} - ,{'queue_name', Queue} % optional to include - ,{'queue_options', ?QUEUE_OPTIONS} % optional to include - ,{'consume_options', ?CONSUME_OPTIONS} % optional to include - ,{'declare_exchanges', Exchanges} - ,{'broker', Broker} - ] - ,[C] - ,[{'debug', [{'install', {fun handle_debug/3, 'mystate'}}] - } - ] - ). - -%%%============================================================================= -%%% gen_server callbacks -%%%============================================================================= - -%%------------------------------------------------------------------------------ -%% @doc Initializes the server. -%% @end -%%------------------------------------------------------------------------------ --spec init([amqp_listener_connection()]) -> {'ok', state()}. -init([#amqp_listener_connection{}=Connection]) -> - {'ok', #state{connection=Connection}}. - -%%------------------------------------------------------------------------------ -%% @doc Handling call messages. -%% @end -%%------------------------------------------------------------------------------ --spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_types:handle_call_ret_state(state()). -handle_call(_Request, _From, State) -> - {'reply', {'error', 'not_implemented'}, State}. - -%%------------------------------------------------------------------------------ -%% @doc Handling cast messages. -%% @end -%%------------------------------------------------------------------------------ --spec handle_cast(any(), state()) -> kz_types:handle_cast_ret_state(state()). -handle_cast({'gen_listener', {'created_queue', _QueueNAme}}, State) -> - {'noreply', State}; -handle_cast({'gen_listener', {'is_consuming', _IsConsuming}}, State) -> - {'noreply', State}; -handle_cast(_Msg, State) -> - lager:debug("inbound listener unhandled cast: ~p", [_Msg]), - {'noreply', State}. - -%%------------------------------------------------------------------------------ -%% @doc Handling all non call/cast messages. -%% @end -%%------------------------------------------------------------------------------ --spec handle_info(any(), state()) -> kz_types:handle_info_ret_state(state()). -handle_info(_Info, State) -> - lager:debug("inbound listener unhandled info: ~p", [_Info]), - {'noreply', State}. - -%%------------------------------------------------------------------------------ -%% @doc Allows listener to pass options to handlers. -%% @end -%%------------------------------------------------------------------------------ --spec handle_event(kz_json:object(), kz_term:proplist()) -> gen_listener:handle_event_return(). -handle_event(_JObj, _State) -> - {'reply', []}. - -%%------------------------------------------------------------------------------ -%% @doc This function is called by a `gen_server' when it is about to -%% terminate. It should be the opposite of `Module:init/1' and do any -%% necessary cleaning up. When it returns, the `gen_server' terminates -%% with Reason. The return value is ignored. -%% -%% @end -%%------------------------------------------------------------------------------ --spec terminate(any(), state()) -> 'ok'. -terminate('shutdown', _State) -> - lager:debug("inbound listener terminating"); -terminate(Reason, #state{connection=Connection}) -> - lager:error("inbound listener unexpected termination : ~p", [Reason]), - kz_util:spawn(fun()-> - timer:sleep(10000), - doodle_inbound_listener_sup:start_inbound_listener(Connection) - end). - -%%------------------------------------------------------------------------------ -%% @doc Convert process state when code is changed. -%% @end -%%------------------------------------------------------------------------------ --spec code_change(any(), state(), any()) -> {'ok', state()}. -code_change(_OldVsn, State, _Extra) -> - {'ok', State}. - --spec format_status(any(), list()) -> []. -format_status(_Opt, [_PDict, _State]) -> []. - --spec handle_debug(A, any(), any()) -> A. -handle_debug(FuncState, _Event, _ProcState) -> FuncState. - -%%%============================================================================= -%%% Internal functions -%%%============================================================================= diff --git a/applications/doodle/src/doodle_listener.erl b/applications/doodle/src/doodle_listener.erl index cd04a1a40fc..004dc218e04 100644 --- a/applications/doodle/src/doodle_listener.erl +++ b/applications/doodle/src/doodle_listener.erl @@ -7,6 +7,11 @@ -behaviour(gen_listener). -export([start_link/0]). + +%% Responders +-export([handle_message/2 + ]). + -export([init/1 ,handle_call/3 ,handle_cast/2 @@ -20,20 +25,30 @@ -define(SERVER, ?MODULE). --record(state, {}). --type state() :: #state{}. - --define(BINDINGS, [{'route', [{'types', ?RESOURCE_TYPES_HANDLED} - ,{'restrict_to', ['account']} - ] - } - ,{'self', []} - ]). --define(RESPONDERS, [{'doodle_route_req', [{<<"dialplan">>, <<"route_req">>}]} - ]). --define(QUEUE_NAME, <<>>). --define(QUEUE_OPTIONS, []). --define(CONSUME_OPTIONS, []). +-type state() :: map(). + +-define(BINDINGS, [{'sms', [{'restrict_to', ['inbound']}]}]). + +-define(QUEUE_NAME, <<"doodle">>). + +-define(QUEUE_OPTIONS, [{'exclusive', 'false'} + ,{'durable', 'true'} + ,{'auto_delete', 'false'} + ,{'arguments', [{<<"x-message-ttl">>, 'infinity'} + ,{<<"x-max-length">>, 'infinity'} + ]} + ]). +-define(CONSUME_OPTIONS, [{'exclusive', 'false'} + ,{'no_ack', 'false'} + ]). + +-define(RESPONDERS, [{{?MODULE, 'handle_message'}, [{<<"message">>, <<"inbound">>}]}]). + +-define(AMQP_PUBLISH_OPTIONS, [{'mandatory', 'true'} + ,{'delivery_mode', 2} + ]). + +-define(ROUTE_TIMEOUT, 'infinity'). %%%============================================================================= %%% API @@ -45,13 +60,15 @@ %%------------------------------------------------------------------------------ -spec start_link() -> kz_types:startlink_ret(). start_link() -> - gen_listener:start_link(?SERVER, [{'bindings', ?BINDINGS} - ,{'responders', ?RESPONDERS} - ,{'queue_name', ?QUEUE_NAME} % optional to include - ,{'queue_options', ?QUEUE_OPTIONS} % optional to include - ,{'consume_options', ?CONSUME_OPTIONS} % optional to include - %%,{basic_qos, 1} % only needed if prefetch controls - ], []). + gen_listener:start_link(?MODULE + ,[{'bindings', ?BINDINGS} + ,{'responders', ?RESPONDERS} + ,{'queue_name', ?QUEUE_NAME} % optional to include + ,{'queue_options', ?QUEUE_OPTIONS} % optional to include + ,{'consume_options', ?CONSUME_OPTIONS} % optional to include + ] + ,[] + ). %%%============================================================================= %%% gen_server callbacks @@ -63,7 +80,7 @@ start_link() -> %%------------------------------------------------------------------------------ -spec init([]) -> {'ok', state()}. init([]) -> - {'ok', #state{}}. + {'ok', #{}}. %%------------------------------------------------------------------------------ %% @doc Handling call messages. @@ -78,7 +95,7 @@ handle_call(_Request, _From, State) -> %% @end %%------------------------------------------------------------------------------ -spec handle_cast(any(), state()) -> kz_types:handle_cast_ret_state(state()). -handle_cast({'gen_listener', {'created_queue', _QueueNAme}}, State) -> +handle_cast({'gen_listener', {'created_queue', _Q}}, State) -> {'noreply', State}; handle_cast({'gen_listener', {'is_consuming', _IsConsuming}}, State) -> {'noreply', State}; @@ -97,7 +114,7 @@ handle_info(_Info, State) -> %% @doc Allows listener to pass options to handlers. %% @end %%------------------------------------------------------------------------------ --spec handle_event(kz_json:object(), kz_term:proplist()) -> gen_listener:handle_event_return(). +-spec handle_event(kz_json:object(), state()) -> gen_listener:handle_event_return(). handle_event(_JObj, _State) -> {'reply', []}. @@ -124,3 +141,238 @@ code_change(_OldVsn, State, _Extra) -> %%%============================================================================= %%% Internal functions %%%============================================================================= + +%%%============================================================================= +%%% Handle Inbound +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec handle_message(kz_json:object(), kz_term:proplist()) -> 'ok'. +handle_message(JObj, Props) -> + Srv = props:get_value('server', Props), + Deliver = props:get_value('deliver', Props), + case kapi_sms:inbound_v(JObj) of + 'true' -> + Context = inbound_context(JObj, Props), + kz_util:put_callid(kz_api_sms:message_id(JObj)), + handle_inbound(Context); + 'false' -> + lager:debug("error validating inbound message : ~p", [JObj]), + gen_listener:nack(Srv, Deliver) + end. + +-spec inbound_context(kz_json:object(), kz_term:proplist()) -> map(). +inbound_context(JObj, Props) -> + Srv = props:get_value('server', Props), + Deliver = props:get_value('deliver', Props), + Basic = props:get_value('basic', Props), + {Number, Inception} = doodle_util:get_inbound_destination(JObj), + #{number => Number + ,inception => Inception + ,request => JObj + ,route_id => kz_api_sms:route_id(JObj) + ,route_type => kz_api_sms:route_type(JObj) + ,basic => Basic + ,deliver => Deliver + ,server => Srv + }. + +-spec handle_inbound(map()) -> 'ok'. +handle_inbound(#{number := Number} = Context0) -> + Routines = [fun custom_vars/1 + ,fun custom_header_token/1 + ,fun lookup_number/1 + ,fun account_from_number/1 + ,fun set_inception/1 + ,fun lookup_mdn/1 + ,fun create_im/1 + ], + case kz_maps:exec(Routines, Context0) of + #{account_id := _AccountId} = Context -> + lager:info("processing inbound sms request ~s in account ~s", [Number, _AccountId]), + route_message(Context); + Context -> + lager:info("unable to determine account for ~s => ~p", [Number, Context]), + %% TODO send system notify ? + ack(Context) + end. + +ack(#{server := Server + ,deliver := Deliver + }) -> gen_listener:ack(Server, Deliver). + +%% nack(#{server := Server +%% ,deliver := Deliver +%% }) -> gen_listener:nack(Server, Deliver). + + +custom_vars(#{authorizing_id := _} = Map) -> Map; +custom_vars(#{request := JObj} = Map) -> + CCVsFilter = [<<"Account-ID">>, <<"Authorizing-ID">>], + CCVs = kz_json:get_json_value(<<"Custom-Channel-Vars">>, JObj, kz_json:new()), + Filtered = [{CCV, V} || CCV <- CCVsFilter, (V = kz_json:get_value(CCV, CCVs)) =/= 'undefined'], + lists:foldl(fun custom_var/2, Map, Filtered). + +custom_var({K,V}, Map) -> + maps:put(kz_term:to_atom(kz_json:normalize_key(K), 'true'), V, Map). + +custom_header_token(#{authorizing_id := _} = Map) -> Map; +custom_header_token(#{request := JObj} = Map) -> + case kz_json:get_ne_binary_value([<<"Custom-SIP-Headers">>, <<"X-AUTH-Token">>], JObj) of + 'undefined' -> Map; + Token -> + custom_header_token(Map, JObj, Token) + end. + +custom_header_token(Map, JObj, Token) -> + case binary:split(Token, <<"@">>, ['global']) of + [AuthorizingId, AccountId | _] -> + AccountRealm = kzd_accounts:fetch_realm(AccountId), + AccountDb = kz_util:format_account_db(AccountId), + case kz_datamgr:open_cache_doc(AccountDb, AuthorizingId) of + {'ok', Doc} -> + Props = props:filter_undefined([{?CCV(<<"Authorizing-Type">>), kz_doc:type(Doc)} + ,{?CCV(<<"Authorizing-ID">>), AuthorizingId} + ,{?CCV(<<"Owner-ID">>), kzd_devices:owner_id(Doc)} + ,{?CCV(<<"Account-ID">>), AccountId} + ,{?CCV(<<"Account-Realm">>), AccountRealm} + ]), + Map#{authorizing_id => AuthorizingId + ,account_id => AccountId + ,request => kz_json:set_values(Props, JObj) + }; + _Else -> + lager:warning("unexpected result reading doc ~s/~s => ~p", [AuthorizingId, AccountId, _Else]), + Map + end; + _Else -> + lager:warning("unexpected result spliting Token => ~p", [_Else]), + Map + end. + +lookup_number(#{account_id := _AccountId} = Map) -> Map; +lookup_number(#{number := Number} = Map) -> + case knm_phone_number:fetch(Number) of + {'error', _R} -> + lager:info("unable to determine account for ~s: ~p", [Number, _R]), + Map; + {'ok', KNumber} -> + Map#{phone_number => KNumber, used_by => knm_phone_number:used_by(KNumber)} + end; +lookup_number(Map) -> + Map. + +account_from_number(#{account_id := _AccountId} = Map) -> Map; +account_from_number(#{phone_number := KNumber, request := JObj} = Map) -> + case knm_phone_number:assigned_to(KNumber) of + 'undefined' -> Map; + AccountId -> + AccountRealm = kzd_accounts:fetch_realm(AccountId), + Props = [{<<"Account-ID">>, AccountId} + ,{?CCV(<<"Account-ID">>), AccountId} + ,{?CCV(<<"Account-Realm">>), AccountRealm} + ,{?CCV(<<"Authorizing-Type">>), <<"resource">>} + ], + Map#{account_id => AccountId + ,request => kz_json:set_values(Props, JObj) + } + end; +account_from_number(Map) -> + Map. + +-spec set_inception(map()) -> map(). +set_inception(#{inception := <<"offnet">>, request := JObj} = Map) -> + Request = kz_json:get_value(<<"From">>, JObj), + Map#{request => kz_json:set_value(?CCV(<<"Inception">>), Request, JObj)}; +set_inception(#{request := JObj} = Map) -> + Map#{request => kz_json:delete_keys([<<"Inception">>, ?CCV(<<"Inception">>)], JObj)}. + +-spec lookup_mdn(map()) -> map(). +lookup_mdn(#{authorizing_id := _AuthorizingId} = Map) -> Map; +lookup_mdn(#{phone_number := KNumber, request := JObj} = Map) -> + Number = knm_phone_number:number(KNumber), + case doodle_util:lookup_mdn(Number) of + {'ok', Id, OwnerId} -> + Props = props:filter_undefined([{?CCV(<<"Authorizing-Type">>), <<"device">>} + ,{?CCV(<<"Authorizing-ID">>), Id} + ,{?CCV(<<"Owner-ID">>), OwnerId} + ]), + JObj1 = kz_json:delete_keys([?CCV(<<"Authorizing-Type">>) + ,?CCV(<<"Authorizing-ID">>) + ], JObj), + Map#{request => kz_json:set_values(Props, JObj1)}; + {'error', _} -> Map + end; +lookup_mdn(Map) -> Map. + +-spec create_im(map()) -> map(). +create_im(#{request:= SmsReq, account_id := _AccountId} = Map) -> + IM = kapps_im:from_payload(SmsReq), + Map#{fetch_id => kapps_im:message_id(IM) + ,message_id => kapps_im:message_id(IM) + ,im => IM + }; +create_im(Map) -> Map. + +%%%============================================================================= +%%% Route Message +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec route_message(kapps_im:call()) -> 'ok'. +route_message(#{im := Im} = Context) -> + AllowNoMatch = allow_no_match(Im), + case kz_flow:lookup(Im) of + %% if NoMatch is false then allow the callflow or if it is true and we are able allowed + %% to use it for this call + {'ok', Flow, NoMatch} when (not NoMatch) + orelse AllowNoMatch -> + route_message(Context, Flow, NoMatch); + {'ok', _, 'true'} -> + lager:info("only available flow is a nomatch for a unauthorized message"), + ack(Context); + {'error', R} -> + lager:info("unable to find flow ~p", [R]), + ack(Context) + end. + +-spec allow_no_match(kapps_im:im()) -> boolean(). +allow_no_match(Im) -> + allow_no_match_type(Im). + +-spec allow_no_match_type(kapps_im:im()) -> boolean(). +allow_no_match_type(Im) -> + case kapps_im:authorizing_type(Im) of + 'undefined' -> 'false'; + <<"resource">> -> 'false'; + <<"sys_info">> -> 'false'; + _ -> 'true' + end. + +-spec route_message(map(), kz_json:object(), boolean()) -> 'ok'. +route_message(#{im := Im} = Context, Flow, NoMatch) -> + lager:info("flow ~s in ~s satisfies request" + ,[kz_doc:id(Flow), kapps_im:account_id(Im)]), + Updaters = [{fun kapps_im:kvs_store_proplist/2 + ,[{'tf_flow_id', kz_doc:id(Flow)} + ,{'tf_flow', kz_json:get_value(<<"flow">>, Flow)} + ,{'tf_no_match', NoMatch} + ] + } + ], + {'ok', Pid} = tf_exe_sup:new(kapps_im:exec(Updaters, Im)), + MonitorRef = erlang:monitor(process, Pid), + receive + {'DOWN', MonitorRef, 'process', Pid, {shutdown, Reason}} -> + lager:info("textflow result : ~p", [Reason]), + ack(Context); + {'DOWN', MonitorRef, 'process', Pid, Reason} -> + lager:info("textflow result : ~p", [Reason]), + ack(Context) + end. diff --git a/applications/doodle/src/doodle_listener_sup.erl b/applications/doodle/src/doodle_listener_sup.erl new file mode 100644 index 00000000000..7ead0aff739 --- /dev/null +++ b/applications/doodle/src/doodle_listener_sup.erl @@ -0,0 +1,79 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2012-2019, 2600Hz +%%% @doc This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(doodle_listener_sup). + +-behaviour(supervisor). + +-include("doodle.hrl"). + +-define(SERVER, ?MODULE). + +-export([start_link/0]). +-export([worker/0]). +-export([init/1]). + +-define(CHILDREN, [?WORKER_ARGS_TYPE('doodle_listener', [], 'temporary')]). + +%% =================================================================== +%% API functions +%% =================================================================== + +%%------------------------------------------------------------------------------ +%% @doc Starts the supervisor +%% @end +%%------------------------------------------------------------------------------ +-spec start_link() -> kz_types:startlink_ret(). +start_link() -> + {'ok', Pid} = supervisor:start_link({'local', ?SERVER}, ?MODULE, []), + _ = init_workers(Pid), + {'ok', Pid}. + +%%------------------------------------------------------------------------------ +%% @doc Random Worker. +%% @end +%%------------------------------------------------------------------------------ +-spec worker() -> {'error', 'no_connections'} | pid(). +worker() -> + Listeners = supervisor:which_children(?SERVER), + case length(Listeners) of + 0 -> {'error', 'no_connections'}; + Size -> + Selected = rand:uniform(Size), + {_, Pid, _, _} = lists:nth(Selected, Listeners), + Pid + end. + +%% =================================================================== +%% Supervisor callbacks +%% =================================================================== + +%%------------------------------------------------------------------------------ +%% @doc Whenever a supervisor is started using supervisor:start_link/[2,3], +%% this function is called by the new process to find out about +%% restart strategy, maximum restart frequency and child +%% specifications. +%% @end +%%------------------------------------------------------------------------------ +-spec init(list()) -> kz_types:sup_init_ret(). +init([]) -> + RestartStrategy = 'simple_one_for_one', + MaxRestarts = 0, + MaxSecondsBetweenRestarts = 1, + SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts}, + {'ok', {SupFlags, ?CHILDREN}}. + +init_workers(Pid) -> + Workers = kapps_config:get_integer(?CONFIG_CAT, <<"listeners">>, 1), + _ = kz_util:spawn(fun() -> [begin + _ = supervisor:start_child(Pid, []), + timer:sleep(500) + end + || _N <- lists:seq(1, Workers) + ] + end). diff --git a/applications/doodle/src/doodle_maintenance.erl b/applications/doodle/src/doodle_maintenance.erl index 9d79641050c..f496c63baee 100644 --- a/applications/doodle/src/doodle_maintenance.erl +++ b/applications/doodle/src/doodle_maintenance.erl @@ -7,160 +7,29 @@ -include("doodle.hrl"). --export([send_outbound_sms/2, send_outbound_sms/3, send_outbound_sms/4, send_outbound_sms/5]). +-export([send_outbound_sms/2, send_outbound_sms/3]). -export([flush/0]). --export([check_sms_by_device_id/2, check_sms_by_owner_id/2]). --export([start_check_sms_by_device_id/2, start_check_sms_by_owner_id/2]). --export([start_check_sms_by_account/2]). --export([check_pending_sms_for_outbound_delivery/1]). --export([check_pending_sms_for_delivery/1]). -spec flush() -> 'ok'. flush() -> kz_cache:flush_local(?CACHE_NAME). --spec start_check_sms_by_device_id(kz_term:ne_binary(), kz_term:ne_binary()) -> pid(). -start_check_sms_by_device_id(AccountId, DeviceId) -> - kz_util:spawn(fun check_sms_by_device_id/2, [AccountId, DeviceId]). - --spec start_check_sms_by_owner_id(kz_term:ne_binary(), kz_term:ne_binary()) -> pid(). -start_check_sms_by_owner_id(AccountId, OwnerId) -> - kz_util:spawn(fun check_sms_by_owner_id/2, [AccountId, OwnerId]). - --spec check_sms_by_device_id(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -check_sms_by_device_id(_AccountId, 'undefined') -> 'ok'; -check_sms_by_device_id(AccountId, DeviceId) -> - ViewOptions = [{'endkey', [DeviceId, kz_time:now_s()]}], - case kazoo_modb:get_results(AccountId, <<"sms/deliver_to_device">>, ViewOptions) of - {'ok', []} -> 'ok'; - {'ok', JObjs} -> replay_sms(AccountId, JObjs); - {'error', _R} -> - lager:debug("unable to get sms by device for ~s/~s: ~p", [AccountId, DeviceId, _R]) - end. - --spec check_sms_by_owner_id(kz_term:ne_binary(), kz_term:api_binary()) -> 'ok'. -check_sms_by_owner_id(_AccountId, 'undefined') -> 'ok'; -check_sms_by_owner_id(AccountId, OwnerId) -> - ViewOptions = [{'endkey', [OwnerId, kz_time:now_s()]}], - case kazoo_modb:get_results(AccountId, <<"sms/deliver_to_owner">>, ViewOptions) of - {'ok', []} -> 'ok'; - {'ok', JObjs} -> replay_sms(AccountId, JObjs); - {'error', _R} -> - lager:debug("unable to get sms by owner_id for owner_id ~s in account ~s: ~p", [AccountId, OwnerId, _R]) - end. - --spec start_check_sms_by_account(kz_term:ne_binary(), kz_json:object()) -> pid(). -start_check_sms_by_account(AccountId, JObj) -> - case kz_doc:is_soft_deleted(JObj) - orelse kz_term:is_false(kz_json:get_value(<<"pvt_enabled">>, JObj, 'true')) - of - 'true' -> 'ok'; - 'false' -> check_sms_by_account(AccountId) - end. - --spec check_sms_by_account(kz_term:ne_binary()) -> pid(). -check_sms_by_account(AccountId) -> - kz_util:spawn(fun check_pending_sms_for_delivery/1, [AccountId]), - kz_util:spawn(fun check_queued_sms/1, [AccountId]). - --spec check_pending_sms_for_outbound_delivery(kz_term:ne_binary()) -> pid(). -check_pending_sms_for_outbound_delivery(AccountId) -> - kz_util:spawn(fun check_pending_sms_for_offnet_delivery/1, [AccountId]). - --spec check_pending_sms_for_delivery(kz_term:ne_binary()) -> 'ok'. -check_pending_sms_for_delivery(AccountId) -> - ViewOptions = [{'limit', 100} - ,{'endkey', kz_time:now_s()} - ], - case kazoo_modb:get_results(AccountId, <<"sms/deliver">>, ViewOptions) of - {'ok', []} -> 'ok'; - {'ok', JObjs} -> replay_sms(AccountId, JObjs); - {'error', _R} -> - lager:debug("unable to get sms list for delivery in account ~s : ~p", [AccountId, _R]) - end. - --spec check_queued_sms(kz_term:ne_binary()) -> 'ok'. -check_queued_sms(AccountId) -> - ViewOptions = [{'limit', 100}], - case kazoo_modb:get_results(AccountId, <<"sms/queued">>, ViewOptions) of - {'ok', []} -> 'ok'; - {'ok', JObjs} -> replay_queue_sms(AccountId, JObjs); - {'error', _R} -> - lager:debug("unable to get queued sms list in account ~s : ~p", [AccountId, _R]) - end. - --spec replay_queue_sms(kz_term:ne_binary(), kz_json:objects()) -> 'ok'. -replay_queue_sms(AccountId, JObjs) -> - lager:debug("starting ~B queued sms for account ~s", [length(JObjs), AccountId]), - _ = [spawn_handler(AccountId, JObj) - || JObj <- JObjs - ], - 'ok'. - --spec spawn_handler(kz_term:ne_binary(), kz_json:object()) -> 'ok'. -spawn_handler(AccountId, JObj) -> - DocId = kz_doc:id(JObj), - ?MATCH_MODB_PREFIX(Year,Month,_) = DocId, - AccountDb = kazoo_modb:get_modb(AccountId, Year, Month), - {'ok', Doc} = kz_datamgr:open_doc(AccountDb, DocId), - _ = case kz_json:get_value(<<"pvt_origin">>, Doc) of - <<"api">> -> kz_util:spawn(fun doodle_api:handle_api_sms/2, [AccountDb, DocId]); - _Else -> kz_util:spawn(fun doodle_util:replay_sms/2, [AccountId, DocId]) - end, - timer:sleep(200). - --spec check_pending_sms_for_offnet_delivery(kz_term:ne_binary()) -> 'ok'. -check_pending_sms_for_offnet_delivery(AccountId) -> - ViewOptions = [{'limit', 100} - ,{'endkey', kz_time:now_s()} - ], - case kazoo_modb:get_results(AccountId, <<"sms/deliver_to_offnet">>, ViewOptions) of - {'ok', []} -> 'ok'; - {'ok', JObjs} -> replay_sms(AccountId, JObjs); - {'error', _R} -> - lager:debug("unable to get sms list for offnet delivery in account ~s : ~p", [AccountId, _R]) - end. - --spec replay_sms(kz_term:ne_binary(), kz_json:objects()) -> 'ok'. -replay_sms(AccountId, JObjs) -> - lager:debug("starting sms delivery for account ~s", [AccountId]), - F = fun (JObj) -> - _ = doodle_util:replay_sms(AccountId, kz_doc:id(JObj)), - timer:sleep(200) - end, - lists:foreach(F, JObjs). - --define(DEFAULT_ROUTEID, - kapps_config:get_ne_binary(?CONFIG_CAT, <<"default_test_route_id">>, <<"syneverse">>)). -define(DEFAULT_FROM, kapps_config:get_ne_binary(?CONFIG_CAT, <<"default_test_from_number">>, <<"15552220001">>)). -spec send_outbound_sms(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. send_outbound_sms(To, Msg) -> - send_outbound_sms(To, ?DEFAULT_FROM, ?DEFAULT_ROUTEID, Msg). - --spec send_outbound_sms(kz_term:ne_binary(), kz_term:ne_binary(), pos_integer()) -> 'ok'. -send_outbound_sms(To, Msg, Times) -> - send_outbound_sms(To, ?DEFAULT_FROM, ?DEFAULT_ROUTEID, Msg, Times). + send_outbound_sms(To, ?DEFAULT_FROM, Msg). --spec send_outbound_sms(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -send_outbound_sms(To, From, RouteId, Msg) -> +-spec send_outbound_sms(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. +send_outbound_sms(To, From, Msg) -> Payload = [{<<"Message-ID">>, kz_binary:rand_hex(16)} ,{<<"System-ID">>, kz_util:node_name()} - ,{<<"Route-ID">>, RouteId} ,{<<"From">>, From} ,{<<"To">>, kz_term:to_binary(To)} ,{<<"Body">>, Msg} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ], - kapps_sms_command:send_amqp_sms(Payload). + kz_amqp_worker:cast(Payload, fun kapi_sms:publish_outbound/1). --spec send_outbound_sms(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), pos_integer()) -> 'ok'. -send_outbound_sms(To, From, RouteId, Msg, Times) -> - F = fun (X) -> - MSG = <<"MSG - ", (kz_term:to_binary(X))/binary, " => ", Msg/binary>>, - send_outbound_sms(To, From, RouteId, MSG), - timer:sleep(2000) - end, - lists:foreach(F, lists:seq(1, kz_term:to_integer(Times))). diff --git a/applications/doodle/src/doodle_notify_handler.erl b/applications/doodle/src/doodle_notify_handler.erl deleted file mode 100644 index 57f9aab6e80..00000000000 --- a/applications/doodle/src/doodle_notify_handler.erl +++ /dev/null @@ -1,54 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @copyright (C) 2013-2019, 2600Hz -%%% @doc Handlers for various AMQP payloads -%%% @author Luis Azedo -%%% @end -%%%----------------------------------------------------------------------------- --module(doodle_notify_handler). - --export([handle_req/2]). - --include("doodle.hrl"). - --spec handle_req(kz_json:object(), kz_term:proplist()) -> 'ok'. -handle_req(JObj, _Props) -> - 'true' = kapi_registration:success_v(JObj), - _ = kz_util:put_callid(JObj), - Username = kz_json:get_value(<<"Username">>, JObj), - Realm = kz_json:get_value(<<"Realm">>, JObj), - case kapps_util:get_account_by_realm(Realm) of - {'ok', AccountDb} -> handle_account_req(AccountDb, Username); - {'error', 'not_found'} -> handle_no_account_req(Realm, Username) - end. - --spec handle_account_req(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -handle_account_req(AccountDb, Username) -> - AccountId = kz_util:format_account_id(AccountDb), - case cf_util:endpoint_id_by_sip_username(AccountDb, Username) of - {'ok', EndpointId} -> - case kz_endpoint:get(EndpointId, AccountDb) of - {'ok', Endpoint} -> - OwnerId = kz_json:get_value(<<"owner_id">>, Endpoint), - doodle_maintenance:start_check_sms_by_device_id(AccountId, EndpointId), - doodle_maintenance:start_check_sms_by_owner_id(AccountId, OwnerId); - {'error', _E} -> - lager:debug("error getting Endpoint ~s from account db ~s : ~p" - ,[EndpointId, AccountDb, _E]) - end; - {'error', _E} -> - lager:debug("error getting EndpointId with username ~s from account db ~s : ~p" - ,[Username, AccountDb, _E]) - end. - --spec handle_no_account_req(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -handle_no_account_req(Realm, Username) -> - case doodle_util:endpoint_from_sipdb(Realm, Username) of - {'ok', Endpoint} -> - AccountId = kz_doc:account_id(Endpoint), - EndpointId = kz_doc:id(Endpoint), - OwnerId = kz_json:get_value(<<"owner_id">>, Endpoint), - doodle_maintenance:start_check_sms_by_device_id(AccountId, EndpointId), - doodle_maintenance:start_check_sms_by_owner_id(AccountId, OwnerId); - {'error', _E} -> - lager:debug("error finding ~s@~s endpoint in sip_db : ~p", [Username, Realm, _E]) - end. diff --git a/applications/doodle/src/doodle_outbound_handler.erl b/applications/doodle/src/doodle_outbound_handler.erl deleted file mode 100644 index 2fa5da8bcef..00000000000 --- a/applications/doodle/src/doodle_outbound_handler.erl +++ /dev/null @@ -1,74 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @copyright (C) 2010-2019, 2600Hz -%%% @doc Handlers for various AMQP payloads -%%% @author James Aimonetti -%%% @end -%%%----------------------------------------------------------------------------- --module(doodle_outbound_handler). - --export([handle_req/2]). - --include("doodle.hrl"). - --spec handle_req(kz_json:object(), kz_term:proplist()) -> 'ok'. -handle_req(JObj, _Props) -> - 'true' = kapi_sms:outbound_v(JObj), - _ = kz_util:put_callid(JObj), - maybe_route(JObj). - --spec maybe_route(kz_json:object()) -> any(). -maybe_route(JObj) -> - Funs = [fun account_has_sms/1 - ,fun trusted_application/1 - ,fun number/1 - ,fun exchange/1 - ], - Route = kzd_sms:route_id(JObj, <<"default">>), - route(kz_maps:exec(Funs, #{payload => JObj, route => Route})). - --spec route(map()) -> any(). -route(#{payload := Payload}) -> - kapps_sms_command:send_amqp_sms(Payload, ?OUTBOUND_POOL); -route(_Map) -> - lager:debug_unsafe("not routing sms ~p", [_Map]). - -account_has_sms(#{payload := JObj} = Map) -> - %% TODO should this verify services ? - AccountId = kzd_sms:account_id(JObj), - case kzd_accounts:fetch(AccountId, 'accounts') of - {'ok', Account} -> - Map#{account_id => AccountId, enabled => kz_json:is_true(<<"pvt_outbound_sms">>, Account)}; - _ -> Map#{account_id => AccountId, enabled => 'false'} - end. - -trusted_application(#{payload := JObj} = Map) -> - AppId = kzd_sms:route_id(JObj), - case kz_json:get_ne_binary_value([<<"default">>, <<"outbound">>, <<"trusted_apps">>, AppId], config()) of - 'undefined' -> Map; - RouteId -> Map#{enabled => 'true', route => RouteId, payload => kzd_sms:set_route_id(JObj, RouteId)} - end. - -exchange(#{payload := JObj} = Map) -> - Exchange = kz_json:get_ne_binary_value(?OUTBOUND_EXCHANGE_ARG(<<"name">>), config(), kz_binary:rand_hex(16)), - Map#{payload => kz_json:set_value(<<"Exchange-ID">>, Exchange, JObj)}. - -number(#{enabled := 'false'} = Map) -> Map; -number(#{payload := JObj} = Map) -> - case knm_phone_number:fetch(kzd_sms:caller_id_number(JObj)) of - {'ok', Num} -> - case route_from_number(Num) of - 'undefined' -> Map#{number => Num}; - RouteId -> Map#{number => Num, route => RouteId, payload => kzd_sms:set_route_id(JObj, RouteId)} - end; - _ -> Map - end. - -route_from_number(Num) -> - Mod = knm_phone_number:module_name(Num), - kz_json:get_ne_binary_value([<<"default">>, <<"outbound">>, <<"knm">>, Mod], config()). - -config() -> - case kapps_config:get_category(?APP_NAME) of - {'ok', JObj} -> JObj; - _ -> kz_json:new() - end. diff --git a/applications/doodle/src/doodle_route_req.erl b/applications/doodle/src/doodle_route_req.erl deleted file mode 100644 index c6365d8fe85..00000000000 --- a/applications/doodle/src/doodle_route_req.erl +++ /dev/null @@ -1,159 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @copyright (C) 2013-2019, 2600Hz -%%% @doc Handlers for various AMQP payloads -%%% @author Luis Azedo -%%% @end -%%%----------------------------------------------------------------------------- --module(doodle_route_req). - --export([handle_req/2]). - --include("doodle.hrl"). - --define(DEFAULT_ROUTE_WIN_TIMEOUT, 3000). --define(ROUTE_WIN_TIMEOUT_KEY, <<"route_win_timeout">>). --define(ROUTE_WIN_TIMEOUT, kapps_config:get_integer(?CONFIG_CAT, ?ROUTE_WIN_TIMEOUT_KEY, ?DEFAULT_ROUTE_WIN_TIMEOUT)). - --spec handle_req(kz_json:object(), kz_term:proplist()) -> 'ok'. -handle_req(JObj, Props) -> - 'true' = kapi_route:req_v(JObj), - Call = kapps_call:from_route_req(JObj), - case is_binary(kapps_call:account_id(Call)) - of - 'false' -> 'ok'; - 'true' -> - lager:info("received a request asking if doodle can route this message"), - AllowNoMatch = allow_no_match(Call), - case kz_flow:lookup(Call) of - %% if NoMatch is false then allow the callflow or if it is true and we are able allowed - %% to use it for this call - {'ok', Flow, NoMatch} when (not NoMatch) - orelse AllowNoMatch -> - NewFlow = maybe_prepend_preflow(Call, Flow), - maybe_reply_to_req(JObj, Props, Call, NewFlow, NoMatch); - {'ok', _, 'true'} -> - lager:info("only available flow is a nomatch for a unauthorized call", []); - {'error', R} -> - lager:info("unable to find flow ~p", [R]) - end - end. - --spec maybe_prepend_preflow(kapps_call:call(), kz_json:object()) -> kz_json:object(). -maybe_prepend_preflow(Call, CallFlow) -> - AccountDb = kapps_call:account_db(Call), - case kzd_accounts:fetch(AccountDb) of - {'error', _E} -> - lager:warning("could not open account doc ~s : ~p", [AccountDb, _E]), - CallFlow; - {'ok', Doc} -> - case kzd_accounts:preflow_id(Doc) of - 'undefined' -> CallFlow; - PreflowId -> kzd_callflow:prepend_preflow(CallFlow, PreflowId) - end - end. - --spec allow_no_match(kapps_call:call()) -> boolean(). -allow_no_match(Call) -> - kapps_call:custom_channel_var(<<"Referred-By">>, Call) =/= 'undefined' - orelse allow_no_match_type(Call). - --spec allow_no_match_type(kapps_call:call()) -> boolean(). -allow_no_match_type(Call) -> - case kapps_call:authorizing_type(Call) of - 'undefined' -> 'false'; - <<"resource">> -> 'false'; - <<"sys_info">> -> 'false'; - _ -> 'true' - end. - --spec maybe_reply_to_req(kz_json:object(), kz_term:proplist(), kapps_call:call(), kz_json:object(), boolean()) -> - 'ok'. -maybe_reply_to_req(JObj, Props, Call, Flow, NoMatch) -> - lager:info("callflow ~s in ~s satisfies request" - ,[kz_doc:id(Flow), kapps_call:account_id(Call)]), - {Name, Cost} = bucket_info(Call, Flow), - - case kz_buckets:consume_tokens(?APP_NAME, Name, Cost) of - 'false' -> - lager:debug("bucket ~s doesn't have enough tokens(~b needed) for this call", [Name, Cost]); - 'true' -> - ControllerQ = props:get_value('queue', Props), - UpdatedCall = update_call(Flow, NoMatch, ControllerQ, Call, JObj), - send_route_response(Flow, JObj, UpdatedCall) - end. - --spec bucket_info(kapps_call:call(), kz_json:object()) -> {kz_term:ne_binary(), pos_integer()}. -bucket_info(Call, Flow) -> - case kz_json:get_value(<<"pvt_bucket_name">>, Flow) of - 'undefined' -> {bucket_name_from_call(Call, Flow), bucket_cost(Flow)}; - Name -> {Name, bucket_cost(Flow)} - end. - --spec bucket_name_from_call(kapps_call:call(), kz_json:object()) -> kz_term:ne_binary(). -bucket_name_from_call(Call, Flow) -> - <<(kapps_call:account_id(Call))/binary, ":", (kz_doc:id(Flow))/binary>>. - --spec bucket_cost(kz_json:object()) -> pos_integer(). -bucket_cost(Flow) -> - Min = kapps_config:get_integer(?CONFIG_CAT, <<"min_bucket_cost">>, 1), - case kz_json:get_integer_value(<<"pvt_bucket_cost">>, Flow) of - 'undefined' -> Min; - N when N < Min -> Min; - N -> N - end. - --spec send_route_response(kz_json:object(), kz_json:object(), kapps_call:call()) -> 'ok'. -send_route_response(_Flow, JObj, Call) -> - lager:info("doodle knows how to route the message! sending sms response"), - Resp = props:filter_undefined([{?KEY_MSG_ID, kz_api:msg_id(JObj)} - ,{?KEY_MSG_REPLY_ID, kapi_route:fetch_id(JObj)} - ,{<<"Routes">>, []} - ,{<<"Method">>, <<"sms">>} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ]), - ServerId = kz_api:server_id(JObj), - Publisher = fun(P) -> kapi_route:publish_resp(ServerId, P) end, - case kz_amqp_worker:call(Resp - ,Publisher - ,fun kapi_route:win_v/1 - ,?ROUTE_WIN_TIMEOUT - ) - of - {'ok', RouteWin} -> - lager:info("doodle has received a route win, taking control of the text"), - doodle_route_win:execute_text_flow(RouteWin, kapps_call:from_route_win(RouteWin, Call)); - {'error', _E} -> - lager:info("doodle didn't received a route win, exiting : ~p", [_E]) - end. - --spec update_call(kz_json:object(), boolean(), kz_term:ne_binary(), kapps_call:call(), kz_json:object()) -> kapps_call:call(). -update_call(Flow, NoMatch, ControllerQ, Call, JObj) -> - Updaters = [{fun kapps_call:kvs_store_proplist/2 - ,[{'cf_flow_id', kz_doc:id(Flow)} - ,{'cf_flow', kz_json:get_value(<<"flow">>, Flow)} - ,{'cf_capture_group', kz_json:get_ne_value(<<"capture_group">>, Flow)} - ,{'cf_no_match', NoMatch} - ,{'cf_metaflow', kz_json:get_value(<<"metaflows">>, Flow)} - ,{'flow_status', <<"queued">>} - ] - } - ,{fun kapps_call:set_controller_queue/2, ControllerQ} - ,{fun kapps_call:set_application_name/2, ?APP_NAME} - ,{fun kapps_call:set_application_version/2, ?APP_VERSION} - ,fun(C) -> cache_resource_types(Flow, C, JObj) end - ], - kapps_call:exec(Updaters, Call). - --spec cache_resource_types(kz_json:object(), kapps_call:call(), kz_json:object()) -> kapps_call:call(). -cache_resource_types(Flow, Call, JObj) -> - lists:foldl(fun(K, C1) -> - kapps_call:kvs_store(K, kz_json:get_value(K, JObj), C1) - end - ,Call - ,cache_resource_types(kapps_call:resource_type(Call), Flow, Call, JObj) - ). - --spec cache_resource_types(kz_term:ne_binary(), kz_json:object(), kapps_call:call(), kz_json:object()) -> kz_term:ne_binaries(). -cache_resource_types(<<"sms">>, _Flow, _Call, _JObj) -> - [<<"Message-ID">>, <<"Body">>]; -cache_resource_types(_Other, _Flow, _Call, _JObj) -> []. diff --git a/applications/doodle/src/doodle_route_win.erl b/applications/doodle/src/doodle_route_win.erl deleted file mode 100644 index 1ad4cfeea46..00000000000 --- a/applications/doodle/src/doodle_route_win.erl +++ /dev/null @@ -1,281 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @copyright (C) 2011-2019, 2600Hz -%%% @doc handler for route wins, bootstraps callflow execution -%%% @author Karl Anderson -%%% @end -%%%----------------------------------------------------------------------------- --module(doodle_route_win). - --include("doodle.hrl"). - --define(JSON(L), kz_json:from_list(L)). - --define(DEFAULT_SERVICES, ?JSON([{<<"audio">>, ?JSON([{<<"enabled">>, 'true'}])} - ,{<<"video">>, ?JSON([{<<"enabled">>, 'true'}])} - ,{<<"sms">>, ?JSON([{<<"enabled">>, 'true'}])} - ])). - --define(DEFAULT_LANGUAGE, <<"en-US">>). --define(DEFAULT_UNAVAILABLE_MESSAGE, <<"sms service unavailable">>). --define(DEFAULT_UNAVAILABLE_MESSAGE_NODE, kz_json:from_list([{?DEFAULT_LANGUAGE, ?DEFAULT_UNAVAILABLE_MESSAGE}])). --define(RESTRICTED_MSG, <<"endpoint is restricted from making this call">>). --define(SCHEDULED(Call), kapps_call:custom_channel_var(<<"Scheduled-Delivery">>, 0, Call)). - --export([execute_text_flow/2]). - --spec execute_text_flow(kz_json:object(), kapps_call:call()) -> 'ok' | {'ok', pid()}. -execute_text_flow(JObj, Call) -> - case should_restrict_call(Call) of - 'true' -> - lager:debug("endpoint is restricted from sending this text, terminate", []), - _ = send_service_unavailable(JObj, Call), - _Call = doodle_util:save_sms(doodle_util:set_flow_error(<<"error">>, ?RESTRICTED_MSG, Call)), - 'ok'; - 'false' -> - maybe_scheduled_delivery(JObj, Call, ?SCHEDULED(Call) , kz_time:now_s()) - end. - --spec maybe_scheduled_delivery(kz_json:object(), kapps_call:call(), integer(), integer()) -> - kapps_call:call() | {'ok', pid()}. -maybe_scheduled_delivery(_JObj, Call, DeliveryAt, Now) - when DeliveryAt > Now -> - lager:info("scheduling sms delivery"), - Schedule = [{<<"rule">>, 1} - ,{<<"rule_start_time">>, DeliveryAt} - ,{<<"start_time">>, DeliveryAt} - ,{<<"attempts">>, 0} - ,{<<"total_attempts">>, 0} - ], - Call1 = kapps_call:kvs_store(<<"flow_schedule">>, kz_json:from_list(Schedule), Call), - doodle_util:save_sms(doodle_util:set_flow_status(<<"pending">>, Call1)); -maybe_scheduled_delivery(JObj, Call, _, _) -> - lager:info("setting initial information about the text"), - bootstrap_textflow_executer(JObj, Call). - --spec should_restrict_call(kapps_call:call()) -> boolean(). -should_restrict_call(Call) -> - case kz_endpoint:get(Call) of - {'error', _R} -> - lager:debug("error getting kz_endpoint for the sms : ~p", [_R]), - 'false'; - {'ok', JObj} -> maybe_service_unavailable(JObj, Call) - end. - --spec maybe_service_unavailable(kz_json:object(), kapps_call:call()) -> boolean(). -maybe_service_unavailable(JObj, Call) -> - Id = kz_doc:id(JObj), - Services = kz_json:merge( - kz_json:get_value(<<"services">>, JObj, ?DEFAULT_SERVICES), - kz_json:get_value(<<"pvt_services">>, JObj, kz_json:new())), - case kz_json:is_true([<<"sms">>,<<"enabled">>], Services, 'true') of - 'true' -> - maybe_account_service_unavailable(JObj, Call); - 'false' -> - lager:debug("device ~s does not have sms service enabled", [Id]), - 'true' - end. - --spec maybe_account_service_unavailable(kz_json:object(), kapps_call:call()) -> boolean(). -maybe_account_service_unavailable(JObj, Call) -> - AccountId = kapps_call:account_id(Call), - {'ok', Doc} = kzd_accounts:fetch(AccountId), - Services = kz_json:merge( - kz_json:get_value(<<"services">>, Doc, ?DEFAULT_SERVICES), - kz_json:get_value(<<"pvt_services">>, Doc, kz_json:new())), - case kz_json:is_true([<<"sms">>,<<"enabled">>], Services, 'true') of - 'true' -> - maybe_closed_group_restriction(JObj, Call); - 'false' -> - lager:debug("account ~s does not have sms service enabled", [AccountId]), - 'true' - end. - --spec maybe_closed_group_restriction(kz_json:object(), kapps_call:call()) -> boolean(). -maybe_closed_group_restriction(JObj, Call) -> - case kz_json:get_value([<<"call_restriction">>, <<"closed_groups">>, <<"action">>], JObj) of - <<"deny">> -> enforce_closed_groups(JObj, Call); - _Else -> maybe_classification_restriction(JObj, Call) - end. - --spec maybe_classification_restriction(kz_json:object(), kapps_call:call()) -> boolean(). -maybe_classification_restriction(JObj, Call) -> - Number = kapps_call:request_user(Call), - Classification = knm_converters:classify(Number), - lager:debug("classified number as ~s, testing for call restrictions", [Classification]), - kz_json:get_value([<<"call_restriction">>, Classification, <<"action">>], JObj) =:= <<"deny">>. - --spec enforce_closed_groups(kz_json:object(), kapps_call:call()) -> boolean(). -enforce_closed_groups(JObj, Call) -> - case get_callee_extension_info(Call) of - 'undefined' -> - lager:info("dialed number is not an extension, using classification restrictions", []), - maybe_classification_restriction(JObj, Call); - {<<"user">>, CalleeId} -> - lager:info("dialed number is user ~s extension, checking groups", [CalleeId]), - Groups = kz_attributes:groups(Call), - CallerGroups = get_caller_groups(Groups, JObj, Call), - CalleeGroups = get_group_associations(CalleeId, Groups), - sets:size(sets:intersection(CallerGroups, CalleeGroups)) =:= 0; - {<<"device">>, CalleeId} -> - lager:info("dialed number is device ~s extension, checking groups", [CalleeId]), - Groups = kz_attributes:groups(Call), - CallerGroups = get_caller_groups(Groups, JObj, Call), - maybe_device_groups_intersect(CalleeId, CallerGroups, Groups, Call) - end. - --spec get_caller_groups(kz_json:objects(), kz_json:object(), kapps_call:call()) -> sets:set(). -get_caller_groups(Groups, JObj, Call) -> - Ids = [kapps_call:authorizing_id(Call) - ,kz_json:get_value(<<"owner_id">>, JObj) - | kz_json:get_keys([<<"hotdesk">>, <<"users">>], JObj) - ], - lists:foldl(fun('undefined', Set) -> Set; - (Id, Set) -> - get_group_associations(Id, Groups, Set) - end, sets:new(), Ids). - --spec maybe_device_groups_intersect(kz_term:ne_binary(), sets:set(), kz_json:objects(), kapps_call:call()) -> boolean(). -maybe_device_groups_intersect(CalleeId, CallerGroups, Groups, Call) -> - CalleeGroups = get_group_associations(CalleeId, Groups), - case sets:size(sets:intersection(CallerGroups, CalleeGroups)) =:= 0 of - 'false' -> 'false'; - 'true' -> - %% In this case the callee-id is a device id, find out if - %% the owner of the device shares any groups with the caller - UserIds = kz_attributes:owner_ids(CalleeId, Call), - UsersGroups = lists:foldl(fun(UserId, Set) -> - get_group_associations(UserId, Groups, Set) - end, sets:new(), UserIds), - sets:size(sets:intersection(CallerGroups, UsersGroups)) =:= 0 - end. - --spec get_group_associations(kz_term:ne_binary(), kz_json:objects()) -> sets:set(). -get_group_associations(Id, Groups) -> - get_group_associations(Id, Groups, sets:new()). - --spec get_group_associations(kz_term:ne_binary(), kz_json:objects(), sets:set()) -> sets:set(). -get_group_associations(Id, Groups, Set) -> - lists:foldl(fun(Group, S) -> - case kz_json:get_value([<<"value">>, Id], Group) of - 'undefined' -> S; - _Else -> - GroupId = kz_doc:id(Group), - sets:add_element(GroupId, S) - end - end, Set, Groups). - --spec get_callee_extension_info(kapps_call:call()) -> {kz_term:ne_binary(), kz_term:ne_binary()} | 'undefined'. -get_callee_extension_info(Call) -> - Flow = kapps_call:kvs_fetch('cf_flow', Call), - FirstModule = kz_json:get_value(<<"module">>, Flow), - FirstId = kz_json:get_value([<<"data">>, <<"id">>], Flow), - SecondModule = kz_json:get_value([<<"_">>, <<"module">>], Flow), - case (FirstModule =:= <<"device">> - orelse - FirstModule =:= <<"user">>) - andalso - (SecondModule =:= <<"voicemail">> - orelse - SecondModule =:= 'undefined') - andalso - FirstId =/= 'undefined' - of - 'true' -> {FirstModule, FirstId}; - 'false' -> 'undefined' - end. - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec bootstrap_textflow_executer(kz_json:object(), kapps_call:call()) -> {'ok', pid()}. -bootstrap_textflow_executer(_JObj, Call) -> - Routines = [fun store_owner_id/1 - ,fun update_ccvs/1 - %% all funs above here return kapps_call:call() - ,fun execute_textflow/1 - ], - lists:foldl(fun(F, C) -> F(C) end, Call, Routines). - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec store_owner_id(kapps_call:call()) -> kapps_call:call(). -store_owner_id(Call) -> - OwnerId = kz_attributes:owner_id(Call), - kapps_call:kvs_store('owner_id', OwnerId, Call). - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec update_ccvs(kapps_call:call()) -> kapps_call:call(). -update_ccvs(Call) -> - CallerIdType = case kapps_call:inception(Call) of - 'undefined' -> <<"internal">>; - _Else -> <<"external">> - end, - {CIDNumber, CIDName} = kz_attributes:caller_id(CallerIdType, Call), - lager:info("bootstrapping with caller id type ~s: \"~s\" ~s" - ,[CallerIdType, CIDName, CIDNumber]), - Props = props:filter_undefined( - [{<<"Caller-ID-Name">>, CIDName} - ,{<<"Caller-ID-Number">>, CIDNumber} - | get_incoming_security(Call) - ]), - kapps_call:set_custom_channel_vars(Props, Call). - --spec get_incoming_security(kapps_call:call()) -> kz_term:proplist(). -get_incoming_security(Call) -> - case kz_endpoint:get(Call) of - {'error', _R} -> []; - {'ok', JObj} -> - kz_json:to_proplist( - kz_endpoint:encryption_method_map(kz_json:new(), JObj) - ) - end. - -%%------------------------------------------------------------------------------ -%% @doc executes the found call flow by starting a new doodle_exe process under the -%% doodle_exe_sup tree. -%% @end -%%------------------------------------------------------------------------------ --spec execute_textflow(kapps_call:call()) -> {'ok', pid()}. -execute_textflow(Call) -> - lager:info("message has been setup, beginning to process the message"), - doodle_exe_sup:new(Call). - --spec send_service_unavailable(kz_json:object(), kapps_call:call()) -> kapps_call:call(). -send_service_unavailable(_JObj, Call) -> - Routines = [fun store_owner_id/1 - ,fun update_ccvs/1 - ,fun set_service_unavailable_message/1 - ,fun set_sms_sender/1 - ,fun send_reply_msg/1 - ], - kapps_call:exec(Routines, Call). - --spec set_service_unavailable_message(kapps_call:call()) -> kapps_call:call(). -set_service_unavailable_message(Call) -> - {'ok', Endpoint} = kz_endpoint:get(Call), - Language = kz_json:get_value(<<"language">>, Endpoint, ?DEFAULT_LANGUAGE), - TextNode = kapps_config:get_json(?CONFIG_CAT, <<"unavailable_message">>, ?DEFAULT_UNAVAILABLE_MESSAGE_NODE), - Text = kz_json:get_value(Language, TextNode, ?DEFAULT_UNAVAILABLE_MESSAGE), - kapps_call:kvs_store(<<"Body">>, Text, Call). - --spec send_reply_msg(kapps_call:call()) -> kapps_call:call(). -send_reply_msg(Call) -> - EndpointId = kapps_call:authorizing_id(Call), - Options = kz_json:set_value(<<"can_call_self">>, 'true', kz_json:new()), - _ = case kz_endpoint:build(EndpointId, Options, Call) of - {'error', Msg}=_E -> - lager:debug("error getting endpoint for reply unavailable service ~s : ~p", [EndpointId, Msg]); - {'ok', Endpoints} -> - kapps_sms_command:b_send_sms(Endpoints, Call) - end, - Call. - --spec set_sms_sender(kapps_call:call()) -> kapps_call:call(). -set_sms_sender(Call) -> - kapps_call:set_from(<<"sip:sms-service@",(kapps_call:from_realm(Call))/binary>>, Call). diff --git a/applications/doodle/src/doodle_sup.erl b/applications/doodle/src/doodle_sup.erl index a1473e0872b..ed8dbe3f9d1 100644 --- a/applications/doodle/src/doodle_sup.erl +++ b/applications/doodle/src/doodle_sup.erl @@ -14,21 +14,17 @@ -define(SERVER, ?MODULE). --define(ORIGIN_BINDINGS, [[{'db', ?KZ_SIP_DB } - ,{'type', <<"device">>} - ] +-define(ORIGIN_BINDINGS, [[{'type', <<"callflow">>}] + ,[{'type', <<"textflow">>}] ]). -define(CACHE_PROPS, [{'origin_bindings', ?ORIGIN_BINDINGS} ]). -define(CHILDREN, [?CACHE_ARGS(?CACHE_NAME, ?CACHE_PROPS) - ,?WORKER('doodle_listener') - ,?WORKER('doodle_shared_listener') - ,?SUPER('doodle_event_handler_sup') - ,?SUPER('doodle_exe_sup') - ,?SUPER('doodle_inbound_listener_sup') - ,?DOODLE_POOL_NAME_ARGS(?OUTBOUND_POOL, [outbound_pool_args()]) + ,?WORKER('tf_exe_listener') + ,?SUPER('tf_exe_sup') + ,?WORKER('doodle_listener_sup') ]). %%============================================================================== @@ -62,39 +58,3 @@ init([]) -> MaxSecondsBetweenRestarts = 10, SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts}, {'ok', {SupFlags, ?CHILDREN}}. - -outbound_pool_args() -> - PoolSize = kz_json:get_integer_value(?OUTBOUND_POOL_ARG(<<"size">>), config(), 5), - PoolOverflow = kz_json:get_integer_value(?OUTBOUND_POOL_ARG(<<"overflow">>), config(), 5), - PoolThreshold = kz_json:get_integer_value(?OUTBOUND_POOL_ARG(<<"threshold">>), config(), 5), - PoolServerConfirms = kz_json:is_true(?OUTBOUND_POOL_ARG(<<"confirms">>), config(), 'true'), - Broker = kz_json:get_ne_binary_value(?OUTBOUND_POOL_ARG(<<"broker">>), config(), ?DEFAULT_BROKER), - Exchange = kz_json:get_ne_binary_value(?OUTBOUND_EXCHANGE_ARG(<<"name">>), config(), kz_binary:rand_hex(16)), - ExchangeType = kz_json:get_ne_binary_value(?OUTBOUND_EXCHANGE_ARG(<<"type">>), config(), <<"topic">>), - ExchangeOptions = kz_json:get_json_value(?OUTBOUND_EXCHANGE_ARG(<<"options">>), config(), ?DEFAULT_EXCHANGE_OPTIONS_JOBJ), - Exchanges = [{Exchange, ExchangeType, amqp_exchange_options(ExchangeOptions)}], - [{'worker_module', 'kz_amqp_worker'} - ,{'name', {'local', ?OUTBOUND_POOL}} - ,{'size', PoolSize} - ,{'max_overflow', PoolOverflow} - ,{'strategy', 'fifo'} - ,{'neg_resp_threshold', PoolThreshold} - ,{'amqp_broker', Broker} - ,{'amqp_queuename_start', ?OUTBOUND_POOL} - ,{'amqp_bindings', []} - ,{'amqp_exchanges', Exchanges} - ,{'amqp_server_confirms', PoolServerConfirms} - ]. - -config() -> - case kapps_config:get_category(?APP_NAME) of - {'ok', JObj} -> JObj; - _ -> kz_json:new() - end. - --spec amqp_exchange_options(kz_term:api_object()) -> kz_term:proplist(). -amqp_exchange_options('undefined') -> []; -amqp_exchange_options(JObj) -> - [{kz_term:to_atom(K, 'true'), V} - || {K, V} <- kz_json:to_proplist(JObj) - ]. diff --git a/applications/doodle/src/doodle_util.erl b/applications/doodle/src/doodle_util.erl index d06a4b1d503..06536df6bd3 100644 --- a/applications/doodle/src/doodle_util.erl +++ b/applications/doodle/src/doodle_util.erl @@ -18,23 +18,8 @@ -define(MDN_VIEW, <<"mobile/listing_by_mdn">>). -define(CONVERT_MDN, 'true'). --define(RESCHEDULE_COUNTERS, [<<"attempts">>, <<"total_attempts">>]). - --export([endpoint_id_from_sipdb/2, get_endpoint_id_from_sipdb/2]). --export([endpoint_from_sipdb/2, get_endpoint_from_sipdb/2]). --export([save_sms/1, save_sms/2]). --export([replay_sms/2]). --export([get_sms_body/1, set_sms_body/2]). --export([set_flow_status/2, set_flow_status/3]). --export([set_flow_error/2, set_flow_error/3, clear_flow_error/1]). --export([handle_bridge_failure/2, handle_bridge_failure/3]). --export([sms_status/1, sms_status/2]). --export([get_callee_id/2 , set_callee_id/2]). --export([get_caller_id/2 , get_caller_id/3]). --export([set_caller_id/2, set_caller_id/3]). -export([get_inbound_destination/1]). -export([lookup_mdn/1]). --export([maybe_reschedule_sms/1, maybe_reschedule_sms/2, maybe_reschedule_sms/3]). %%============================================================================== %% API functions @@ -44,341 +29,6 @@ %% @doc %% @end %%------------------------------------------------------------------------------ --spec set_sms_body(kz_term:ne_binary(), kapps_call:call()) -> kapps_call:call(). -set_sms_body(Body, Call) -> - kapps_call:kvs_store(<<"Body">>, Body, Call). - --spec get_sms_body(kapps_call:call()) -> kz_term:ne_binary(). -get_sms_body(Call) -> - kapps_call:kvs_fetch(<<"Body">>, Call). - --spec set_flow_status(kz_term:ne_binary() | {binary(), binary()}, kapps_call:call()) -> kapps_call:call(). -set_flow_status({Status, Message}, Call) -> - Props = [{<<"flow_status">>, Status} - ,{<<"flow_message">>, Message} - ], - kapps_call:kvs_store_proplist(Props, Call); -set_flow_status(Status, Call) -> - kapps_call:kvs_store(<<"flow_status">>, Status, Call). - --spec set_flow_status(kz_term:ne_binary(), kz_term:ne_binary(), kapps_call:call()) -> kapps_call:call(). -set_flow_status(Status, Message, Call) -> - Props = [{<<"flow_status">>, Status} - ,{<<"flow_message">>, Message} - ], - kapps_call:kvs_store_proplist(Props, Call). - --spec set_flow_error(kz_term:api_binary() | {binary(), binary()}, kapps_call:call()) -> kapps_call:call(). -set_flow_error({Status, Error}, Call) -> - Props = [{<<"flow_status">>, Status} - ,{<<"flow_error">>, Error} - ], - kapps_call:kvs_store_proplist(Props, Call); -set_flow_error(Error, Call) -> - set_flow_error(<<"pending">>, Error, Call). - --spec set_flow_error(kz_term:ne_binary(), kz_term:api_binary(), kapps_call:call()) -> kapps_call:call(). -set_flow_error(Status, Error, Call) -> - Props = [{<<"flow_status">>, Status} - ,{<<"flow_error">>, Error} - ], - kapps_call:kvs_store_proplist(Props, Call). - --spec clear_flow_error(kapps_call:call()) -> kapps_call:call(). -clear_flow_error(Call) -> - Props = [<<"flow_status">>, <<"flow_error">>], - kapps_call:kvs_erase(Props, Call). - --spec get_sms_revision(kapps_call:call()) -> kz_term:api_binary(). -get_sms_revision(Call) -> - case kapps_call:kvs_fetch(<<"_rev">>, Call) of - 'undefined' -> kapps_call:custom_channel_var(<<"Doc-Revision">>, Call); - Rev -> Rev - end. - --spec set_sms_revision(kz_term:api_binary(), kapps_call:call()) -> kapps_call:call(). -set_sms_revision(Rev, Call) -> - kapps_call:kvs_store(<<"_rev">>, Rev, Call). - --spec save_sms(kapps_call:call()) -> kapps_call:call(). -save_sms(Call) -> - Id = kapps_call:kvs_fetch('sms_docid', kapps_call:custom_channel_var(<<"Doc-ID">>, Call), Call), - save_sms(kz_json:new(), Id, Call). - --spec save_sms(kz_json:object(), kapps_call:call()) -> kapps_call:call(). -save_sms(JObj, Call) -> - Id = kapps_call:kvs_fetch('sms_docid', kapps_call:custom_channel_var(<<"Doc-ID">>, Call), Call), - save_sms(JObj, Id, Call). - --spec save_sms(kz_json:object(), kz_term:api_binary(), kapps_call:call()) -> kapps_call:call(). -save_sms(JObj, 'undefined', Call) -> - {Year, Month, _} = erlang:date(), - SmsDocId = kz_term:to_binary( - io_lib:format("~B~s-~s", - [Year - ,kz_date:pad_month(Month) - ,kapps_call:call_id(Call) - ]) - ), - UpdatedCall = kapps_call:kvs_store('sms_docid', SmsDocId, Call), - Doc = kz_doc:set_created(kz_json:new(), kz_time:now_s()), - save_sms(JObj, SmsDocId, Doc, UpdatedCall); -save_sms(JObj, DocId, Call) -> - AccountId = kapps_call:account_id(Call), - ?MATCH_MODB_PREFIX(Year,Month,_) = DocId, - {'ok', Doc} = kazoo_modb:open_doc(AccountId, DocId, Year, Month), - save_sms(JObj, DocId, Doc, Call). - --spec save_sms(kz_json:object(), kz_term:api_binary(), kz_json:object(), kapps_call:call()) -> - kapps_call:call(). -save_sms(JObj, ?MATCH_MODB_PREFIX(Year,Month,_) = DocId, Doc, Call) -> - AccountId = kapps_call:account_id(Call), - AccountMODB = kazoo_modb:get_modb(AccountId, Year, Month), - OwnerId = kapps_call:owner_id(Call), - AuthType = kapps_call:authorizing_type(Call), - AuthId = kapps_call:authorizing_id(Call), - Body = get_sms_body(Call), - Bits = bit_size(Body), - To = kapps_call:to(Call), - From = kapps_call:from(Call), - Request = kapps_call:request(Call), - [ToUser, ToRealm] = binary:split(To, <<"@">>), - [FromUser, FromRealm] = binary:split(From, <<"@">>), - [RequestUser, RequestRealm] = binary:split(Request, <<"@">>), - MessageId = kz_json:get_value(<<"Message-ID">>, JObj), - Rev = get_sms_revision(Call), - Opts = props:filter_undefined([{'rev', Rev}]), - Created = kz_doc:created(JObj, kz_time:now_s()), - Modified = kz_time:now_s(), - Status = kapps_call:kvs_fetch(<<"flow_status">>, <<"queued">>, Call), - Schedule = kapps_call:kvs_fetch(<<"flow_schedule">>, Call), - Props = props:filter_empty( - [{<<"_id">>, DocId} - ,{<<"pvt_type">>, <<"sms">>} - ,{<<"account_id">>, AccountId} - ,{<<"pvt_account_id">>, AccountId} - ,{<<"pvt_account_db">>, AccountMODB} - ,{<<"owner_id">>, OwnerId} - ,{<<"pvt_owner_id">>, OwnerId} - ,{<<"authorization_type">>, AuthType} - ,{<<"authorization_id">>, AuthId} - ,{<<"pvt_authorization_type">>, AuthType} - ,{<<"pvt_authorization_id">>, AuthId} - ,{<<"pvt_target_device_id">>, kapps_call:kvs_fetch(<<"target_device_id">>, Call)} - ,{<<"pvt_target_owner_id">>, kapps_call:kvs_fetch(<<"target_owner_id">>, Call)} - ,{<<"to">>, To} - ,{<<"to_user">>, ToUser} - ,{<<"to_realm">>, ToRealm} - ,{<<"from">>, From} - ,{<<"from_user">>, FromUser} - ,{<<"from_realm">>, FromRealm} - ,{<<"request">>, Request} - ,{<<"request_user">>, RequestUser} - ,{<<"request_realm">>, RequestRealm} - ,{<<"body">>, Body} - ,{<<"bits">>, Bits} - ,{<<"message_id">>, MessageId} - ,{<<"pvt_created">>, Created} - ,{<<"pvt_modified">>, Modified} - ,{<<"pvt_schedule">>, Schedule} - ,{<<"pvt_status">>, Status} - ,{<<"call_id">>, kapps_call:call_id_direct(Call)} - ,{<<"pvt_call">>, kapps_call:to_json(remove_keys(Call))} - ,{<<"_rev">>, Rev} - ]), - JObjDoc = kz_json:set_values(Props, Doc), - case kazoo_modb:save_doc(AccountMODB, JObjDoc, Opts) of - {'ok', Saved} -> - kapps_call:kvs_store(<<"_rev">>, kz_doc:revision(Saved), Call); - {'error', E} -> - lager:error("error saving sms doc : ~p", [E]), - Call - end. - --define(REMOVE_KEYS, [<<"_rev">>, <<"flow_schedule">>]). - --spec remove_keys(kapps_call:call()) -> kapps_call:call(). -remove_keys(Call) -> - remove_keys(Call, ?REMOVE_KEYS). - --spec remove_keys(kapps_call:call(), list()) -> kapps_call:call(). -remove_keys(Call, Keys) -> - lists:foldl(fun kapps_call:kvs_erase/2, Call, Keys). - --spec endpoint_id_from_sipdb(kz_term:ne_binary(), kz_term:ne_binary()) -> - {'ok', kz_term:ne_binary()} | - {'error', any()}. -endpoint_id_from_sipdb(Realm, Username) -> - case kz_cache:peek_local(?CACHE_NAME, ?SIP_ENDPOINT_ID_KEY(Realm, Username)) of - {'ok', _}=Ok -> Ok; - {'error', 'not_found'} -> - get_endpoint_id_from_sipdb(Realm, Username) - end. - --spec get_endpoint_id_from_sipdb(kz_term:ne_binary(), kz_term:ne_binary()) -> - {'ok', kz_term:ne_binary(), kz_term:ne_binary()} | - {'error', any()}. -get_endpoint_id_from_sipdb(Realm, Username) -> - ViewOptions = [{'key', [kz_term:to_lower_binary(Realm) - ,kz_term:to_lower_binary(Username) - ] - }], - case kz_datamgr:get_single_result(?KZ_SIP_DB, <<"credentials/lookup">>, ViewOptions) of - {'ok', JObj} -> - EndpointId = kz_doc:id(JObj), - AccountDb = kz_json:get_value([<<"value">>, <<"account_db">>], JObj), - CacheProps = [{'origin', {'db', ?KZ_SIP_DB, EndpointId}}], - kz_cache:store_local(?CACHE_NAME, ?SIP_ENDPOINT_ID_KEY(Realm, Username), {AccountDb, EndpointId}, CacheProps), - {'ok', EndpointId}; - {'error', _R}=E -> - lager:warning("unable to lookup sip username ~s for owner ids: ~p", [Username, _R]), - E - end. - --spec endpoint_from_sipdb(kz_term:ne_binary(), kz_term:ne_binary()) -> - {'ok', kz_json:object()} | - {'error', any()}. -endpoint_from_sipdb(Realm, Username) -> - case kz_cache:peek_local(?CACHE_NAME, ?SIP_ENDPOINT_KEY(Realm, Username)) of - {'ok', _}=Ok -> Ok; - {'error', 'not_found'} -> - get_endpoint_from_sipdb(Realm, Username) - end. - --spec get_endpoint_from_sipdb(kz_term:ne_binary(), kz_term:ne_binary()) -> - {'ok', kz_json:object()} | - {'error', any()}. -get_endpoint_from_sipdb(Realm, Username) -> - ViewOptions = [{'key', [kz_term:to_lower_binary(Realm) - ,kz_term:to_lower_binary(Username) - ] - } - ,'include_docs' - ], - case kz_datamgr:get_single_result(?KZ_SIP_DB, <<"credentials/lookup">>, ViewOptions) of - {'ok', JObj} -> - EndpointId = kz_doc:id(JObj), - CacheProps = [{'origin', {'db', ?KZ_SIP_DB, EndpointId}}], - Doc = kz_json:get_value(<<"doc">>, JObj), - kz_cache:store_local(?CACHE_NAME, ?SIP_ENDPOINT_ID_KEY(Realm, Username), Doc, CacheProps), - {'ok', Doc}; - {'error', _R}=E -> - lager:warning("lookup sip username ~s in sipdb failed: ~p", [Username, _R]), - E - end. - --spec replay_sms(kz_term:ne_binary(), kz_term:ne_binary()) -> any(). -replay_sms(AccountId, DocId) -> - lager:debug("trying to replay sms ~s for account ~s",[DocId, AccountId]), - {'ok', Doc} = kazoo_modb:open_doc(AccountId, DocId), - Flow = kz_json:get_value(<<"pvt_call">>, Doc), - Schedule = kz_json:get_value(<<"pvt_schedule">>, Doc), - Rev = kz_doc:revision(Doc), - replay_sms_flow(AccountId, DocId, Rev, Flow, Schedule). - --spec replay_sms_flow(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:api_object(), kz_term:api_object()) -> any(). -replay_sms_flow(_AccountId, _DocId, _Rev, 'undefined', _) -> 'ok'; -replay_sms_flow(AccountId, <<_:7/binary, CallId/binary>> = DocId, Rev, JObj, Schedule) -> - lager:debug("replaying sms ~s for account ~s",[DocId, AccountId]), - Routines = [{fun kapps_call:set_call_id/2, CallId} - ,fun(C) -> set_sms_revision(Rev, C) end - ,fun(C) -> set_flow_status(<<"resumed">>, C) end - ,{fun kapps_call:kvs_store/3, <<"flow_schedule">>, Schedule} - ], - - Call = kapps_call:exec(Routines, kapps_call:from_json(JObj)), - kapps_call:put_callid(Call), - lager:info("doodle received sms resume for ~s of account ~s, taking control", [DocId, AccountId]), - doodle_route_win:execute_text_flow(JObj, Call). - --spec sms_status(kz_term:api_object()) -> kz_term:ne_binary(). -sms_status('undefined') -> <<"pending">>; -sms_status(JObj) -> - DeliveryCode = kz_json:get_value(<<"Delivery-Result-Code">>, JObj), - Status = kz_json:get_value(<<"Status">>, JObj), - sms_status(DeliveryCode, Status). - --spec sms_status(kz_term:api_binary(), kz_term:api_binary()) -> kz_term:ne_binary(). -sms_status(<<"sip:", Code/binary>>, Status) -> sms_status(Code, Status); -sms_status(<<"200">>, _) -> <<"delivered">>; -sms_status(<<"202">>, _) -> <<"accepted">>; -sms_status(_, <<"Success">>) -> <<"completed">>; -sms_status(_, _) -> <<"pending">>. - -%%------------------------------------------------------------------------------ -%% @doc Look for children branches to handle the failure replies of -%% certain actions, like cf_sms_offnet and cf_sms_resources -%% @end -%%------------------------------------------------------------------------------ --spec handle_bridge_failure({'fail' | 'error', kz_json:object() | atom()} | kz_term:api_binary(), kapps_call:call()) -> - 'ok' | 'not_found'. -handle_bridge_failure({'fail', Reason}, Call) -> - {Cause, Code} = kapps_util:get_call_termination_reason(Reason), - handle_bridge_failure(Cause, Code, Call); -handle_bridge_failure('undefined', _) -> - 'not_found'; -handle_bridge_failure(<<_/binary>> = Failure, Call) -> - case doodle_exe:attempt(Failure, Call) of - {'attempt_resp', 'ok'} -> - lager:info("found child branch to handle failure: ~s", [Failure]), - 'ok'; - {'attempt_resp', _} -> - 'not_found' - end; -handle_bridge_failure(_, _Call) -> 'not_found'. - --spec handle_bridge_failure(kz_term:api_binary(), kz_term:api_binary(), kapps_call:call()) -> - 'ok' | 'not_found'. -handle_bridge_failure(Cause, Code, Call) -> - lager:info("attempting to find failure branch for ~s:~s", [Code, Cause]), - case (handle_bridge_failure(Cause, Call) =:= 'ok') - orelse (handle_bridge_failure(Code, Call) =:= 'ok') of - 'true' -> 'ok'; - 'false' -> 'not_found' - end. - --spec get_caller_id(kz_json:object(), kapps_call:call()) -> {kz_term:api_binary(), kz_term:api_binary()}. -get_caller_id(Data, Call) -> - get_caller_id(Data, <<"external">>, Call). - --spec get_caller_id(kz_json:object(), binary(), kapps_call:call()) -> {kz_term:api_binary(), kz_term:api_binary()}. -get_caller_id(Data, Default, Call) -> - Type = kz_json:get_value(<<"caller_id_type">>, Data, Default), - kz_attributes:caller_id(Type, Call). - --spec set_caller_id(kz_json:object() | binary(), kapps_call:call()) -> kapps_call:call(). -set_caller_id(CIDNumber, Call) - when is_binary(CIDNumber) -> - set_caller_id(CIDNumber, CIDNumber, Call); -set_caller_id(Data, Call) -> - {CIDNumber, CIDName} = get_caller_id(Data, Call), - set_caller_id(CIDNumber, CIDName, Call). - --spec set_caller_id(binary(), binary(), kapps_call:call()) -> kapps_call:call(). -set_caller_id(CIDNumber, CIDName, Call) -> - Props = props:filter_empty( - [{<<"Caller-ID-Name">>, CIDName} - ,{<<"Caller-ID-Number">>, CIDNumber} - ]), - Routines = [{fun kapps_call:set_caller_id_number/2, CIDNumber} - ,{fun kapps_call:set_caller_id_name/2, CIDName} - ,{fun kapps_call:set_custom_channel_vars/2, Props} - ], - kapps_call:exec(Routines, Call). - --spec get_callee_id(binary(), kapps_call:call()) -> {kz_term:api_binary(), kz_term:api_binary()}. -get_callee_id(EndpointId, Call) -> - kz_attributes:callee_id(EndpointId, Call). - --spec set_callee_id(binary(), kapps_call:call()) -> kapps_call:call(). -set_callee_id(EndpointId, Call) -> - {CIDNumber, CIDName} = get_callee_id(EndpointId, Call), - Props = props:filter_empty( - [{<<"Callee-ID-Name">>, CIDName} - ,{<<"Callee-ID-Number">>, CIDNumber} - ]), - kapps_call:set_custom_channel_vars(Props, Call). - -spec get_inbound_field(kz_term:ne_binary()) -> kz_term:ne_binaries(). get_inbound_field(Inception) -> case Inception of @@ -430,8 +80,9 @@ fetch_mdn_result(AccountId, Num) -> OwnerId = kz_json:get_value([<<"value">>, <<"owner_id">>], JObj), lager:debug("~s is associated with mobile device ~s in account ~s", [Num, Id, AccountId]), cache_mdn_result(AccountDb, Id, OwnerId); + {'error', 'not_found'}=E -> E; {'error', _R}=E -> - lager:debug("could not fetch mdn for ~p: ~p", [Num, _R]), + lager:warning("could not fetch mdn for ~p: ~p", [Num, _R]), E end. @@ -453,173 +104,3 @@ mdn_from_e164(<<"+1", Number/binary>>) -> Number; mdn_from_e164(<<"1", Number/binary>>) -> Number; mdn_from_e164(Number) -> Number. --spec maybe_reschedule_sms(kapps_call:call()) -> 'ok'. -maybe_reschedule_sms(Call) -> - maybe_reschedule_sms(<<>>, <<>>, Call). - --spec maybe_reschedule_sms(kz_term:api_binary(), kapps_call:call()) -> 'ok'. -maybe_reschedule_sms(<<"sip:", Code/binary>>, Call) -> - maybe_reschedule_sms(Code, <<>>, Call); -maybe_reschedule_sms(Code, Call) -> - maybe_reschedule_sms(Code, <<>>, Call). - --spec maybe_reschedule_sms(kz_term:api_binary(), kz_term:api_binary(), kapps_call:call()) -> 'ok'. -maybe_reschedule_sms(Code, 'undefined', Call) -> - maybe_reschedule_sms(Code, set_flow_error(<<"unknown error">>, Call)); -maybe_reschedule_sms(<<"sip:", Code/binary>>, Message, Call) -> - maybe_reschedule_sms(Code, Message, Call); -maybe_reschedule_sms(Code, Message, Call) -> - maybe_reschedule_sms(Code, Message, kapps_call:account_id(Call), set_flow_error(Message, Call)). - --spec maybe_reschedule_sms(kz_term:api_binary(), kz_term:api_binary(), kz_term:ne_binary(), kapps_call:call()) -> 'ok'. -maybe_reschedule_sms(Code, Message, AccountId, Call) -> - put('call', Call), - Rules = kapps_account_config:get_global(AccountId, ?CONFIG_CAT, <<"reschedule">>, kz_json:new()), - Schedule = kz_json:set_values([{<<"code">>, Code} - ,{<<"reason">>, Message} - ] - ,kapps_call:kvs_fetch(<<"flow_schedule">>, kz_json:new(), Call) - ), - case apply_reschedule_logic(kz_json:get_values(Rules), Schedule) of - 'no_rule' -> - lager:debug("no rules configured for account-id ~s", [AccountId]), - doodle_exe:stop(set_flow_status(<<"error">>, Call)); - 'end_rules' -> - lager:debug("end rules configured for account-id ~s", [AccountId]), - doodle_exe:stop(set_flow_status(<<"error">>,Call)); - NewSchedule -> - doodle_exe:stop(kapps_call:kvs_store(<<"flow_schedule">>, NewSchedule, Call)) - end. - --spec inc_counters(kz_json:object(), list()) -> kz_json:object(). -inc_counters(JObj, Counters) -> - lists:foldl(fun inc_counter/2, JObj, Counters). - --spec inc_counter(binary(), kz_json:object()) -> kz_json:object(). -inc_counter(Key, JObj) -> - kz_json:set_value(Key, kz_json:get_integer_value(Key, JObj, 0) + 1, JObj). - --spec apply_reschedule_logic({kz_json:json_terms(), kz_json:path()}, kz_json:object()) -> - 'no_rule' | 'end_rules' | kz_json:object(). -apply_reschedule_logic({[], []}, _JObj) -> 'no_rule'; -apply_reschedule_logic(Rules, JObj) -> - Step = kz_json:get_integer_value(<<"rule">>, JObj, 1), - apply_reschedule_logic(Rules, inc_counters(JObj, ?RESCHEDULE_COUNTERS), Step). - --spec apply_reschedule_logic({kz_json:json_terms(), kz_json:path()}, kz_json:object(), integer()) -> - 'no_rule' | 'end_rules' | kz_json:object(). -apply_reschedule_logic({_Vs, Ks}, _JObj, Step) - when Step > length(Ks) -> 'end_rules'; -apply_reschedule_logic({Vs, Ks}, JObj, Step) -> - Rules = {lists:sublist(Vs, Step, length(Vs)), lists:sublist(Ks, Step, length(Ks))}, - apply_reschedule_rules(Rules, JObj, Step). - --spec apply_reschedule_rules({kz_json:json_terms(), kz_json:path()}, kz_json:object(), integer()) -> - kz_json:object() | 'end_rules'. -apply_reschedule_rules({[], _}, _JObj, _Step) -> 'end_rules'; -apply_reschedule_rules({[Rule | Rules], [Key | Keys]}, JObj, Step) -> - case apply_reschedule_step(kz_json:get_values(Rule), JObj) of - 'no_match' -> - NewObj = kz_json:set_values( - [{<<"rule_start_time">>, kz_time:now_s()} - ,{<<"attempts">>, 0} - ], JObj), - apply_reschedule_rules({Rules, Keys}, NewObj, Step+1); - Schedule -> kz_json:set_values( - [{<<"rule">>, Step} - ,{<<"rule_name">>, Key} - ], Schedule) - end. - --spec apply_reschedule_step({kz_json:json_terms(), kz_json:path()}, kz_json:object()) -> - 'no_match' | kz_json:object(). -apply_reschedule_step({[], []}, JObj) -> JObj; -apply_reschedule_step({[Value | Values], [Key | Keys]}, JObj) -> - case apply_reschedule_rule(Key, Value, JObj) of - 'no_match' -> 'no_match'; - Schedule -> apply_reschedule_step({Values, Keys}, Schedule) - end. - --spec apply_reschedule_rule(kz_term:ne_binary(), any(), kz_json:object()) -> 'no_match' | kz_json:object(). -apply_reschedule_rule(<<"error">>, ErrorObj, JObj) -> - Codes = kz_json:get_value(<<"code">>, ErrorObj, []), - XCodes = kz_json:get_value(<<"xcode">>, ErrorObj, []), - Reasons = kz_json:get_value(<<"reason">>, ErrorObj, []), - XReasons = kz_json:get_value(<<"xreason">>, ErrorObj, []), - Code = kz_json:get_value(<<"code">>, JObj, <<>>), - Reason = kz_json:get_value(<<"reason">>, JObj, <<>>), - case (lists:member(Code, Codes) - orelse Codes =:= [] - ) - andalso (lists:member(Reason, Reasons) - orelse Reasons =:= [] - ) - andalso ((not lists:member(Code, XCodes)) - orelse XCodes =:= [] - ) - andalso ((not lists:member(Reason, XReasons)) - orelse XReasons =:= [] - ) - of - 'true' -> JObj; - 'false' -> 'no_match' - end; -apply_reschedule_rule(<<"number">>, Value, JObj) -> - Attempts = kz_json:get_integer_value(<<"attempts">>, JObj, 0), - case Attempts > Value of - 'true' -> 'no_match'; - 'false' -> JObj - end; -apply_reschedule_rule(<<"time">>, IntervalJObj, JObj) -> - {[Value], [Key]} = kz_json:get_values(IntervalJObj), - Start = kz_json:get_value(<<"rule_start_time">>, JObj), - Until = time_rule(Key, Value, Start), - Now = kz_time:now_s(), - case Until > Now of - 'true' -> JObj; - 'false' -> 'no_match' - end; -apply_reschedule_rule(<<"interval">>, IntervalJObj, JObj) -> - {[Value], [Key]} = kz_json:get_values(IntervalJObj), - Next = time_rule(Key, Value, kz_time:now_s()), - kz_json:set_value(<<"start_time">>, Next, JObj); -apply_reschedule_rule(<<"report">>, V, JObj) -> - Call = get('call'), - Error = list_to_binary([kz_json:get_value(<<"code">>, JObj, <<>>) - ," " - ,kz_json:get_value(<<"reason">>, JObj, <<>>) - ]), - - Props = props:filter_undefined( - [{<<"To">>, kapps_call:to_user(Call)} - ,{<<"From">>, kapps_call:from_user(Call)} - ,{<<"Error">>, kz_binary:strip(Error)} - ,{<<"Attempts">>, kz_json:get_value(<<"attempts">>, JObj)} - | safe_to_proplist(V) - ]), - Notify = [{<<"Subject">>, <<"System Alert: SMS Error">>} - ,{<<"Message">>, <<"undelivered sms">>} - ,{<<"Details">>, kz_json:set_values(Props, kz_json:new())} - ,{<<"Account-ID">>, kapps_call:account_id(Call)} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ], - _ = kz_amqp_worker:cast(Notify, fun kapi_notifications:publish_system_alert/1), - JObj; -apply_reschedule_rule(_, _, JObj) -> JObj. - --spec safe_to_proplist(any()) -> kz_term:proplist(). -safe_to_proplist(JObj) -> - safe_to_proplist(kz_json:is_json_object(JObj), JObj). - --spec safe_to_proplist(boolean(), any()) -> kz_term:proplist(). -safe_to_proplist('true', JObj) -> - kz_json:to_proplist(JObj); -safe_to_proplist(_, _) -> []. - --spec time_rule(kz_term:ne_binary(), integer(), integer()) -> integer(). -time_rule(<<"week">>, N, Base) -> Base + N * ?SECONDS_IN_WEEK; -time_rule(<<"day">>, N, Base) -> Base + N * ?SECONDS_IN_DAY; -time_rule(<<"hour">>, N, Base) -> Base + N * ?SECONDS_IN_HOUR; -time_rule(<<"minute">>, N, Base) -> Base + N * ?SECONDS_IN_MINUTE; -time_rule(<<"second">>, N, Base) -> Base + N * 1; -time_rule(_, _N, Base) -> Base. diff --git a/applications/doodle/src/kz_flow.erl b/applications/doodle/src/kz_flow.erl index 8ccc2df9e0d..8e51241ea0d 100644 --- a/applications/doodle/src/kz_flow.erl +++ b/applications/doodle/src/kz_flow.erl @@ -10,7 +10,6 @@ -export([contains_no_match/1]). -include("doodle.hrl"). --include_lib("kazoo_stdlib/include/kazoo_json.hrl"). -record(pattern, {flow_id :: kz_term:ne_binary() ,has_groups :: boolean() @@ -28,9 +27,9 @@ -type lookup_ret() :: {'ok', kzd_flows:doc(), boolean()} | {'error', any()}. --spec lookup(kapps_call:call()) -> lookup_ret(). -lookup(Call) -> - lookup(kapps_call:request_user(Call), kapps_call:account_id(Call)). +-spec lookup(kapps_im:im()) -> lookup_ret(). +lookup(Im) -> + lookup(kapps_im:request_user(Im), kapps_im:account_id(Im)). -spec lookup(kz_term:ne_binary(), kz_term:ne_binary()) -> lookup_ret(). lookup(Number, AccountId) when not is_binary(Number) -> diff --git a/applications/doodle/src/module/cf_sms_device.erl b/applications/doodle/src/module/cf_sms_device.erl deleted file mode 100644 index c681c0e5c11..00000000000 --- a/applications/doodle/src/module/cf_sms_device.erl +++ /dev/null @@ -1,79 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @copyright (C) 2011-2019, 2600Hz -%%% @doc -%%% @author Karl Anderson -%%% @end -%%%----------------------------------------------------------------------------- --module(cf_sms_device). - --include("doodle.hrl"). - --export([handle/2]). - -%%------------------------------------------------------------------------------ -%% @doc Entry point for this module, attempts to call an endpoint as defined -%% in the Data payload. Returns continue if fails to connect or -%% stop when successful. -%% @end -%%------------------------------------------------------------------------------ --spec handle(kz_json:object(), kapps_call:call()) -> 'ok'. -handle(Data, Call1) -> - EndpointId = kz_doc:id(Data), - Call2 = kapps_call:kvs_store(<<"target_device_id">>, EndpointId, Call1), - case build_endpoint(EndpointId, Data, doodle_util:set_callee_id(EndpointId, Call2)) of - {'error', 'do_not_disturb'} = Reason -> - maybe_handle_bridge_failure(Reason, Call1); - {'error', Reason} -> - doodle_exe:continue(doodle_util:set_flow_error(<<"error">>, kz_term:to_binary(Reason), Call1)); - {Endpoints, Call} -> - case kapps_sms_command:b_send_sms(Endpoints, Call) of - {'ok', JObj} -> handle_result(JObj, Call); - {'error', _} = Reason -> maybe_handle_bridge_failure(Reason, Call) - end - end. - --spec handle_result(kz_json:object(), kapps_call:call()) -> 'ok'. -handle_result(JObj, Call) -> - Status = doodle_util:sms_status(JObj), - Call1 = doodle_util:set_flow_status(Status, Call), - handle_result_status(Call1, Status). - --spec handle_result_status(kapps_call:call(), kz_term:ne_binary()) -> 'ok'. -handle_result_status(Call, <<"pending">>) -> - doodle_util:maybe_reschedule_sms(Call); -handle_result_status(Call, _Status) -> - lager:info("completed successful message to the device"), - doodle_exe:stop(Call). - --spec maybe_handle_bridge_failure({'error', any()}, kapps_call:call()) -> 'ok'. -maybe_handle_bridge_failure({_ , R}=Reason, Call) -> - case doodle_util:handle_bridge_failure(Reason, Call) of - 'not_found' -> - doodle_util:maybe_reschedule_sms( - doodle_util:set_flow_status(<<"pending">>, kz_term:to_binary(R), Call)); - 'ok' -> 'ok' - end. - -%%------------------------------------------------------------------------------ -%% @doc Attempts to build the endpoints to reach this device -%% @end -%%------------------------------------------------------------------------------ --spec build_endpoint(kz_term:ne_binary(), kz_json:object(), kapps_call:call()) -> - {'error', atom() | kz_json:object()} | - {'fail', kz_term:ne_binary() | kz_json:object()} | - {kz_json:objects(), kapps_call:call()}. -build_endpoint(EndpointId, Data, Call) -> - Params = kz_json:set_value(<<"source">>, kz_term:to_binary(?MODULE), Data), - case kz_endpoint:build(EndpointId, Params, Call) of - {'error', _}=E -> E; - {'ok', Endpoints} -> maybe_note_owner(Endpoints, Call) - end. - --spec maybe_note_owner(kz_json:objects(), kapps_call:call()) -> - {kz_json:objects(), kapps_call:call()}. -maybe_note_owner([Endpoint]=Endpoints, Call) -> - case kz_json:get_value([<<"Custom-Channel-Vars">>, <<"Owner-ID">>], Endpoint) of - 'undefined' -> {Endpoints, Call}; - OwnerId -> - {Endpoints, kapps_call:kvs_store(<<"target_owner_id">>, OwnerId, Call)} - end. diff --git a/applications/doodle/src/module/cf_sms_resources.erl b/applications/doodle/src/module/cf_sms_resources.erl deleted file mode 100644 index 3c293edfcaa..00000000000 --- a/applications/doodle/src/module/cf_sms_resources.erl +++ /dev/null @@ -1,210 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @copyright (C) 2011-2019, 2600Hz -%%% @doc -%%% @author Karl Anderson -%%% @author Luis Azedo -%%% @end -%%%----------------------------------------------------------------------------- --module(cf_sms_resources). - --include("doodle.hrl"). - --export([handle/2]). - -%%------------------------------------------------------------------------------ -%% @doc Entry point for this module -%% @end -%%------------------------------------------------------------------------------ --spec handle(kz_json:object(), kapps_call:call()) -> 'ok'. -handle(Data, Call1) -> - AccountId = kapps_call:account_id(Call1), - Call = case kapps_call:custom_channel_var(<<"API-Call">>, 'false', Call1) - andalso kapps_account_config:get_global(AccountId, ?CONFIG_CAT, <<"api_preserve_caller_id">>, 'true') - of - 'true' -> doodle_util:set_caller_id(kapps_call:from_user(Call1), Call1); - 'false' -> doodle_util:set_caller_id(Data, Call1) - end, - case kz_amqp_worker:call(build_offnet_request(Data, Call) - ,fun kapi_offnet_resource:publish_req/1 - ,fun kapi_offnet_resource:resp_v/1 - ,30 * ?MILLISECONDS_IN_SECOND - ) - of - {'ok', Res} -> - handle_result(Res, Call); - {'error', E} -> - lager:debug("error executing offnet action : ~p", [E]), - doodle_util:maybe_reschedule_sms(doodle_util:set_flow_error(E, Call)) - end. - --spec handle_result(kz_json:object(), kapps_call:call()) -> 'ok'. -handle_result(JObj, Call) -> - Message = kz_json:get_value(<<"Response-Message">>, JObj), - Code = kz_json:get_value(<<"Response-Code">>, JObj), - Response = kz_json:get_value(<<"Resource-Response">>, JObj), - handle_result(Message, Code, Response, JObj, Call). - --spec handle_result(binary(), binary() - ,kz_json:object(), kz_json:object() - ,kapps_call:call() - ) -> 'ok'. -handle_result(_Message, <<"sip:200">>, Response, _JObj, Call1) -> - Status = doodle_util:sms_status(Response), - Call = doodle_util:set_flow_status(Status, Call1), - handle_result_status(Call, Status); -handle_result(Message, Code, _Response, _JObj, Call) -> - handle_bridge_failure(Message, Code, Call). - --spec handle_result_status(kapps_call:call(), kz_term:ne_binary()) -> 'ok'. -handle_result_status(Call, <<"pending">>) -> - doodle_util:maybe_reschedule_sms(Call); -handle_result_status(Call, _Status) -> - lager:info("completed successful message to the device"), - doodle_exe:stop(Call). - --spec handle_bridge_failure(kz_term:api_binary(), kz_term:api_binary(), kapps_call:call()) -> 'ok'. -handle_bridge_failure(Cause, Code, Call) -> - lager:info("offnet request error, attempting to find failure branch for ~s:~s", [Code, Cause]), - case doodle_util:handle_bridge_failure(Cause, Code, Call) of - 'ok' -> - lager:debug("found bridge failure child"), - doodle_exe:stop(Call); - 'not_found' -> - doodle_util:maybe_reschedule_sms(Code, Cause, Call) - end. - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec build_offnet_request(kz_json:object(), kapps_call:call()) -> kz_term:proplist(). -build_offnet_request(Data, Call) -> - props:filter_undefined( - [{<<"Resource-Type">>, <<"sms">>} - ,{<<"Application-Name">>, <<"sms">>} - ,{<<"Outbound-Caller-ID-Name">>, kapps_call:caller_id_name(Call)} - ,{<<"Outbound-Caller-ID-Number">>, kapps_call:caller_id_number(Call)} - ,{<<"Msg-ID">>, kz_binary:rand_hex(16)} - ,{<<"Call-ID">>, doodle_exe:callid(Call)} - ,{<<"Control-Queue">>, doodle_exe:control_queue(Call)} - ,{<<"Presence-ID">>, kz_attributes:presence_id(Call)} - ,{<<"Account-ID">>, kapps_call:account_id(Call)} - ,{<<"Account-Realm">>, kapps_call:from_realm(Call)} - ,{<<"Timeout">>, kz_json:get_value(<<"timeout">>, Data)} - ,{<<"Format-From-URI">>, kz_json:is_true(<<"format_from_uri">>, Data)} - ,{<<"Hunt-Account-ID">>, get_hunt_account_id(Data, Call)} - ,{<<"Flags">>, get_flags(Data, Call)} - ,{<<"Custom-SIP-Headers">>, get_sip_headers(Data, Call)} - ,{<<"Custom-Channel-Vars">>, kapps_call:custom_channel_vars(Call)} - ,{<<"Custom-Application-Vars">>, kapps_call:custom_application_vars(Call)} - ,{<<"To-DID">>, get_to_did(Data, Call)} - ,{<<"From-URI-Realm">>, get_from_uri_realm(Data, Call)} - ,{<<"Bypass-E164">>, get_bypass_e164(Data)} - ,{<<"Inception">>, get_inception(Call)} - ,{<<"Message-ID">>, kapps_call:kvs_fetch(<<"Message-ID">>, Call)} - ,{<<"Body">>, kapps_call:kvs_fetch(<<"Body">>, Call)} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ]). - --spec get_bypass_e164(kz_json:object()) -> boolean(). -get_bypass_e164(Data) -> - kz_json:is_true(<<"do_not_normalize">>, Data) - orelse kz_json:is_true(<<"bypass_e164">>, Data). - --spec get_from_uri_realm(kz_json:object(), kapps_call:call()) -> kz_term:api_ne_binary(). -get_from_uri_realm(Data, Call) -> - case kz_json:get_ne_value(<<"from_uri_realm">>, Data) of - 'undefined' -> maybe_get_call_from_realm(Call); - Realm -> Realm - end. - --spec maybe_get_call_from_realm(kapps_call:call()) -> kz_term:api_ne_binary(). -maybe_get_call_from_realm(Call) -> - case kapps_call:from_realm(Call) of - <<"norealm">> -> kzd_accounts:fetch_realm(kapps_call:account_id(Call)); - Realm -> Realm - end. - --spec get_hunt_account_id(kz_json:object(), kapps_call:call()) -> kz_term:api_ne_binary(). -get_hunt_account_id(Data, Call) -> - case kz_json:is_true(<<"use_local_resources">>, Data, 'true') of - 'false' -> 'undefined'; - 'true' -> - AccountId = kapps_call:account_id(Call), - kz_json:get_value(<<"hunt_account_id">>, Data, AccountId) - end. - --spec get_to_did(kz_json:object(), kapps_call:call()) -> kz_term:ne_binary(). -get_to_did(Data, Call) -> - case kz_json:is_true(<<"do_not_normalize">>, Data) of - 'false' -> get_to_did(Data, Call, kapps_call:request_user(Call)); - 'true' -> - Request = kapps_call:request(Call), - [RequestUser, _] = binary:split(Request, <<"@">>), - RequestUser - end. - --spec get_to_did(kz_json:object(), kapps_call:call(), kz_term:ne_binary()) -> kz_term:ne_binary(). -get_to_did(_Data, Call, Number) -> - case kz_endpoint:get(Call) of - {'ok', Endpoint} -> - case kz_json:get_value(<<"dial_plan">>, Endpoint, []) of - [] -> Number; - DialPlan -> cf_util:apply_dialplan(Number, DialPlan) - end; - {'error', _ } -> Number - end. - --spec get_sip_headers(kz_json:object(), kapps_call:call()) -> kz_term:api_object(). -get_sip_headers(Data, Call) -> - Routines = [fun(J) -> - case kz_json:is_true(<<"emit_account_id">>, Data) of - 'false' -> J; - 'true' -> - kz_json:set_value(<<"X-Account-ID">>, kapps_call:account_id(Call), J) - end - end - ], - CustomHeaders = kz_json:get_value(<<"custom_sip_headers">>, Data, kz_json:new()), - JObj = lists:foldl(fun(F, J) -> F(J) end, CustomHeaders, Routines), - case kz_term:is_empty(JObj) of - 'true' -> 'undefined'; - 'false' -> JObj - end. - --spec get_flags(kz_json:object(), kapps_call:call()) -> kz_term:ne_binaries() | undefined. -get_flags(Data, Call) -> - Flags = kz_attributes:get_flags(?APP_NAME, Call), - Routines = [fun get_flow_flags/3 - ,fun get_flow_dynamic_flags/3 - ,fun get_resource_flags/3 - ], - lists:foldl(fun(F, A) -> F(Data, Call, A) end, Flags, Routines). - --spec get_flow_flags(kz_json:object(), kapps_call:call(), kz_term:ne_binaries()) -> - kz_term:ne_binaries(). -get_flow_flags(Data, _Call, Flags) -> - case kz_json:get_list_value(<<"outbound_flags">>, Data, []) of - [] -> Flags; - FlowFlags -> FlowFlags ++ Flags - end. - --spec get_flow_dynamic_flags(kz_json:object(), kapps_call:call(), kz_term:ne_binaries()) -> - kz_term:ne_binaries(). -get_flow_dynamic_flags(Data, Call, Flags) -> - case kz_json:get_list_value(<<"dynamic_flags">>, Data) of - 'undefined' -> Flags; - DynamicFlags -> kz_attributes:process_dynamic_flags(DynamicFlags, Flags, Call) - end. - --spec get_resource_flags(kz_json:object(), kapps_call:call(), kz_term:ne_binaries()) -> kz_term:ne_binaries(). -get_resource_flags(JObj, Call, Flags) -> - get_resource_type_flags(kapps_call:resource_type(Call), JObj, Call, Flags). - --spec get_resource_type_flags(kz_term:ne_binary(), kz_json:object(), kapps_call:call(), kz_term:ne_binaries()) -> kz_term:ne_binaries(). -get_resource_type_flags(<<"sms">>, _JObj, _Call, Flags) -> [<<"sms">> | Flags]; -get_resource_type_flags(_Other, _JObj, _Call, Flags) -> Flags. - --spec get_inception(kapps_call:call()) -> kz_term:api_ne_binary(). -get_inception(Call) -> - kz_json:get_value(<<"Inception">>, kapps_call:custom_channel_vars(Call)). diff --git a/applications/doodle/src/module/cf_sms_user.erl b/applications/doodle/src/module/cf_sms_user.erl deleted file mode 100644 index 88cccb2ab62..00000000000 --- a/applications/doodle/src/module/cf_sms_user.erl +++ /dev/null @@ -1,106 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @copyright (C) 2011-2019, 2600Hz -%%% @doc -%%% @author Luis Azedo -%%% @end -%%%----------------------------------------------------------------------------- --module(cf_sms_user). - --include("doodle.hrl"). - --export([handle/2 - ,get_endpoints/3 - ]). - -%%------------------------------------------------------------------------------ -%% @doc Entry point for this module, attempts to call an endpoint as defined -%% in the Data payload. Returns continue if fails to connect or -%% stop when successful. -%% @end -%%------------------------------------------------------------------------------ --spec handle(kz_json:object(), kapps_call:call()) -> 'ok'. -handle(Data, Call1) -> - UserId = kz_doc:id(Data), - Funs = [{fun doodle_util:set_callee_id/2, UserId} - ,{fun kapps_call:kvs_store/3, <<"target_owner_id">>, UserId} - ], - Call = kapps_call:exec(Funs, Call1), - {Endpoints, Dnd} = get_endpoints(UserId, Data, Call), - Strategy = kz_json:get_binary_value(<<"sms_strategy">>, Data, <<"single">>), - case Endpoints =/= [] - andalso kapps_sms_command:b_send_sms(Endpoints, Strategy, Call) - of - 'false' when Dnd =:= 0 -> - lager:notice("user ~s has no endpoints", [UserId]), - doodle_exe:continue(doodle_util:set_flow_error(<<"error">>, <<"user has no endpoints">>, Call)); - 'false' when Dnd > 0 -> - lager:notice("do not disturb user ~s", [UserId]), - maybe_handle_bridge_failure({'error', 'do_not_disturb'}, Call); - {'ok', JObj} -> - handle_result(JObj, Call); - {'error', _R}=Reason -> - lager:info("error bridging to user: ~p", [_R]), - maybe_handle_bridge_failure(Reason, Call) - end. - --spec handle_result(kz_json:object(), kapps_call:call()) -> 'ok'. -handle_result(JObj, Call1) -> - Status = doodle_util:sms_status(JObj), - Call = doodle_util:set_flow_status(Status, Call1), - handle_result_status(Call, Status). - --spec handle_result_status(kapps_call:call(), kz_term:ne_binary()) -> 'ok'. -handle_result_status(Call, <<"pending">>) -> - doodle_util:maybe_reschedule_sms(Call); -handle_result_status(Call, _Status) -> - lager:info("completed successful message to the user"), - doodle_exe:stop(Call). - --spec maybe_handle_bridge_failure(any(), kapps_call:call()) -> 'ok'. -maybe_handle_bridge_failure({_ , R}=Reason, Call) -> - case doodle_util:handle_bridge_failure(Reason, Call) of - 'not_found' -> - doodle_util:maybe_reschedule_sms( - doodle_util:set_flow_status(<<"pending">>, kz_term:to_binary(R), Call) - ); - 'ok' -> 'ok' - end. - -%%------------------------------------------------------------------------------ -%% @doc Loop over the provided endpoints for the callflow and build the -%% json object used in the bridge API -%% Send to endpoint in determined order -%% @end -%%------------------------------------------------------------------------------ --spec get_endpoints(kz_term:api_binary(), kz_json:object(), kapps_call:call()) -> - {kz_json:objects(), non_neg_integer()}. -get_endpoints('undefined', _, _) -> {[], 0}; -get_endpoints(UserId, Data, Call) -> - Params = kz_json:set_value(<<"source">>, kz_term:to_binary(?MODULE), Data), - EndpointIds = kz_attributes:owned_by(UserId, <<"device">>, Call), - {Endpoints, DndCount} = lists:foldr(fun(EndpointId, {Acc, Dnd}) -> - case kz_endpoint:build(EndpointId, Params, Call) of - {'ok', Endpoint} -> {Endpoint ++ Acc, Dnd}; - {'error', 'do_not_disturb'} -> {Acc, Dnd+1}; - {'error', _E} -> {Acc, Dnd} - end - end - ,{[], 0} - ,EndpointIds - ), - SortedEndpoints = sort_endpoints_by_type(Endpoints), - {SortedEndpoints, DndCount}. - --spec sort_endpoints_by_type(kz_json:objects()) -> kz_json:objects(). -sort_endpoints_by_type(Endpoints) -> - lists:sort(fun(EndpointA, EndpointB) -> - EndpointAValue = endpoint_type_sort_value(kz_json:get_value(<<"Endpoint-Type">>, EndpointA)), - EndpointBValue = endpoint_type_sort_value(kz_json:get_value(<<"Endpoint-Type">>, EndpointB)), - (EndpointAValue < EndpointBValue) - end, - Endpoints - ). - --spec endpoint_type_sort_value(binary()) -> 0..1. -endpoint_type_sort_value(<<"amqp">>) -> 0; -endpoint_type_sort_value(_Type) -> 1. diff --git a/applications/doodle/src/module/tf_device.erl b/applications/doodle/src/module/tf_device.erl new file mode 100644 index 00000000000..e2c84adf62b --- /dev/null +++ b/applications/doodle/src/module/tf_device.erl @@ -0,0 +1,52 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2011-2019, 2600Hz +%%% @doc +%%% @author Karl Anderson +%%% +%%% This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(tf_device). + +-include("doodle.hrl"). + +-export([handle/2]). + +%%------------------------------------------------------------------------------ +%% @doc Entry point for this module, attempts to call an endpoint as defined +%% in the Data payload. Returns continue if fails to connect or +%% stop when successful. +%% @end +%%------------------------------------------------------------------------------ +-spec handle(kz_json:object(), kapps_im:im()) -> 'ok'. +handle(Data, Im) -> + EndpointId = kz_doc:id(Data), + case get_endpoint(EndpointId, Data, Im) of + {'ok', []} -> tf_exe:stop(Im, 'no_endpoints'); + {'ok', Endpoints} -> send_sms(Endpoints, Im); + {'error', Reason} -> tf_exe:stop(Im, Reason) + end. + +-spec send_sms(kz_json:objects(), kapps_im:im()) -> 'ok'. +send_sms(Endpoints, Im) -> + case kapps_im_command:send_sms(Endpoints, Im) of + {'ok', JObj} -> tf_exe:stop(Im, tf_util:delivery_status(JObj)); + {'error', Reason} -> tf_exe:stop(Im, Reason) + end. + +%%------------------------------------------------------------------------------ +%% @doc Attempts to build the endpoints to reach this device +%% @end +%%------------------------------------------------------------------------------ +-spec get_endpoint(kz_term:ne_binary(), kz_json:object(), kapps_im:im()) -> + {'error', any()} | + {'ok', kz_json:objects()}. +get_endpoint(EndpointId, Data, Im) -> + Params = kz_json:set_value(<<"source">>, kz_term:to_binary(?MODULE), Data), + case kz_endpoint:get(EndpointId, kapps_im:account_id(Im)) of + {'ok', Endpoint} -> tf_util:build_im_endpoint(Endpoint, Params, Im); + {'error', _}=E -> E + end. diff --git a/applications/doodle/src/module/cf_sms_offnet.erl b/applications/doodle/src/module/tf_offnet.erl similarity index 69% rename from applications/doodle/src/module/cf_sms_offnet.erl rename to applications/doodle/src/module/tf_offnet.erl index feb28df2500..a798a162180 100644 --- a/applications/doodle/src/module/cf_sms_offnet.erl +++ b/applications/doodle/src/module/tf_offnet.erl @@ -5,18 +5,16 @@ %%% @author Luis Azedo %%% @end %%%----------------------------------------------------------------------------- --module(cf_sms_offnet). +-module(tf_offnet). -include("doodle.hrl"). -export([handle/2]). --define(DEFAULT_EVENT_WAIT, 10 * ?MILLISECONDS_IN_SECOND). - %%------------------------------------------------------------------------------ %% @doc Entry point for this module %% @end %%------------------------------------------------------------------------------ --spec handle(kz_json:object(), kapps_call:call()) -> 'ok'. +-spec handle(kz_json:object(), kapps_im:im()) -> 'ok'. handle(Data, Call) -> - cf_sms_resources:handle(kz_json:set_value(<<"use_local_resources">>, 'false', Data), Call). + tf_resources:handle(kz_json:set_value(<<"use_local_resources">>, 'false', Data), Call). diff --git a/applications/doodle/src/module/tf_resources.erl b/applications/doodle/src/module/tf_resources.erl new file mode 100644 index 00000000000..cd43b666cbb --- /dev/null +++ b/applications/doodle/src/module/tf_resources.erl @@ -0,0 +1,60 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2011-2019, 2600Hz +%%% @doc +%%% @author Karl Anderson +%%% @author Luis Azedo +%%% +%%% This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(tf_resources). + +-include("doodle.hrl"). + +-export([handle/2]). + +-define(CID_EXT_KEY, [<<"caller_id">> + ,<<"external">> + ,<<"number">> + ]). + +%%------------------------------------------------------------------------------ +%% @doc Entry point for this module +%% @end +%%------------------------------------------------------------------------------ +-spec handle(kz_json:object(), kapps_im:im()) -> 'ok'. +handle(Data, Im) -> + API = [{<<"Message-ID">>, kapps_im:message_id(Im)} + ,{<<"Body">>, kapps_im:body(Im)} + ,{<<"From">>, get_from_did(Data, Im)} + ,{<<"To">>, get_to_did(Data, Im)} + ,{<<"Account-ID">>, kapps_im:account_id(Im)} + ,{<<"Route-Type">>, <<"offnet">>} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ], + kapi_sms:publish_outbound(API), + tf_exe:stop(Im, 'offnet'). + +-spec get_to_did(kz_json:object(), kapps_im:im()) -> kz_term:ne_binary(). +get_to_did(Data, Im) -> + case kz_json:is_true(<<"do_not_normalize">>, Data) of + 'false' -> get_normalized_did(Im, kapps_im:request_user(Im)); + 'true' -> kapps_im:request_user(Im) + end. + +-spec get_normalized_did(kapps_im:im(), kz_term:ne_binary()) -> kz_term:ne_binary(). +get_normalized_did(Im, Number) -> + case kz_json:get_value(<<"dial_plan">>, kapps_im:endpoint(Im), []) of + [] -> Number; + DialPlan -> knm_converters:normalize(Number, kapps_im:account_id(Im), DialPlan) + end. + +-spec get_from_did(kz_json:object(), kapps_im:im()) -> kz_term:ne_binary(). +get_from_did(_Data, Im) -> + case knm_converters:is_reconcilable(kapps_im:from_user(Im)) of + 'true' -> get_normalized_did(Im, kapps_im:request_user(Im)); + 'false' -> kz_json:get_ne_value(?CID_EXT_KEY, kapps_im:endpoint(Im)) + end. diff --git a/applications/doodle/src/module/tf_user.erl b/applications/doodle/src/module/tf_user.erl new file mode 100644 index 00000000000..126cf7014c8 --- /dev/null +++ b/applications/doodle/src/module/tf_user.erl @@ -0,0 +1,53 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2011-2019, 2600Hz +%%% @doc +%%% @author Luis Azedo +%%% +%%% This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(tf_user). + +-include("doodle.hrl"). + +-export([handle/2]). + +%%------------------------------------------------------------------------------ +%% @doc Entry point for this module, attempts to call an endpoint as defined +%% in the Data payload. Returns continue if fails to connect or +%% stop when successful. +%% @end +%%------------------------------------------------------------------------------ +-spec handle(kz_json:object(), kapps_im:im()) -> 'ok'. +handle(Data, Im) -> + EndpointId = kz_doc:id(Data), + case get_endpoints(EndpointId, Data, Im) of + {'ok', []} -> tf_exe:stop(Im, 'no_endpoints'); + {'ok', Endpoints} -> send_sms(Endpoints, Data, Im); + {'error', Reason} -> tf_exe:stop(Im, Reason) + end. + +-spec send_sms(kz_json:objects(), kz_json:object(), kapps_im:im()) -> 'ok'. +send_sms(Endpoints, Data, Im) -> + Strategy = kz_json:get_ne_binary_value(<<"sms_strategy">>, Data, <<"single">>), + case kapps_im_command:send_sms(Endpoints, Strategy, Im) of + {'ok', JObj} -> tf_exe:stop(Im, tf_util:delivery_status(JObj)); + {'error', Reason} -> tf_exe:stop(Im, Reason) + end. + +%%------------------------------------------------------------------------------ +%% @doc Builds the Endpoints for the user +%% @end +%%------------------------------------------------------------------------------ +-spec get_endpoints(kz_term:ne_binary(), kz_json:object(), kapps_im:im()) -> + {'error', any()} | + {'ok', kz_json:objects()}. +get_endpoints(EndpointId, Data, Im) -> + Params = kz_json:set_value(<<"source">>, kz_term:to_binary(?MODULE), Data), + case kz_endpoint:get(EndpointId, kapps_im:account_id(Im)) of + {'ok', Endpoint} -> tf_util:build_im_endpoint(Endpoint, Params, Im); + {'error', _}=E -> E + end. diff --git a/applications/doodle/src/tf_exe.erl b/applications/doodle/src/tf_exe.erl new file mode 100644 index 00000000000..273b4563d60 --- /dev/null +++ b/applications/doodle/src/tf_exe.erl @@ -0,0 +1,400 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2010-2019, 2600Hz +%%% @doc +%%% @author Karl Anderson +%%% +%%% This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(tf_exe). +-behaviour(gen_server). + +%% API +-export([start_link/2]). +-export([queue_name/1]). +-export([continue/1, continue/2]). +-export([stop/1, stop/2]). + +%% gen_server callbacks +-export([init/1 + ,handle_call/3 + ,handle_cast/2 + ,handle_info/2 + ,terminate/2 + ,code_change/3 + ]). + +-include("doodle.hrl"). +-include_lib("kazoo_stdlib/include/kazoo_json.hrl"). +-include_lib("kazoo_amqp/include/kz_api.hrl"). + +-record(state, {call = kapps_im:new() :: kapps_im:im() + ,flow = kz_json:new() :: kz_json:object() + ,flows = [] :: kz_json:objects() + ,tf_module_pid :: kz_term:api_pid_ref() + ,tf_module_old_pid :: kz_term:api_pid_ref() + ,result = 'undefined' :: atom() + ,queue :: kz_term:api_ne_binary() + ,self = self() :: pid() + }). +-type state() :: #state{}. + +%%%============================================================================= +%%% API +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc Starts the server. +%% @end +%%------------------------------------------------------------------------------ +-spec start_link(kapps_im:im(), map()) -> kz_types:startlink_ret(). +start_link(Im, Context) -> + gen_server:start_link(?MODULE, [Im, Context], []). + +-spec tf_exe_pid(kapps_im:im()) -> pid(). +tf_exe_pid(Im) -> + kapps_im:kvs_fetch('tf_exe_pid', Im). + +-spec continue(kapps_im:im() | pid()) -> 'ok'. +continue(Srv) -> continue(?DEFAULT_CHILD_KEY, Srv). + +-spec continue(kz_term:ne_binary(), kapps_im:im() | pid()) -> 'ok'. +continue(Key, Srv) when is_pid(Srv) -> + gen_server:cast(Srv, {'continue', Key}); +continue(Key, Im) -> + Srv = tf_exe_pid(Im), + continue(Key, Srv). + +-spec stop(kapps_im:im() | pid()) -> 'ok'. +stop(Srv) -> + stop(Srv, 'undefined'). + +-spec stop(kapps_im:im() | pid(), kz_term:api_ne_binary()) -> 'ok'. +stop(Srv, Cause) when is_pid(Srv) -> + gen_server:cast(Srv, {'stop', Cause}); +stop(Im, Cause) -> + Srv = tf_exe_pid(Im), + stop(Srv, Cause). + +-spec queue_name(kapps_im:im() | pid()) -> kz_term:ne_binary(). +queue_name(Srv) when is_pid(Srv) -> + gen_server:call(Srv, 'queue_name'); +queue_name(Im) -> + Srv = tf_exe_pid(Im), + queue_name(Srv). + +%%%============================================================================= +%%% gen_server callbacks +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc Initializes the server. +%% @end +%%------------------------------------------------------------------------------ +-spec init([kapps_im:im() | map()]) -> {'ok', state()}. +init([Im, Context]) -> + process_flag('trap_exit', 'true'), + kapps_im:put_message_id(Im), + gen_server:cast(self(), 'initialize'), + #{channel := Channel, queue := Queue} = Context, + ControllerQ = kapi:encode_pid(Queue), + kz_amqp_channel:consumer_channel(Channel), + Funs = [{fun kapps_im:kvs_store/3, 'consumer_pid', kz_amqp_channel:consumer_pid()} + ,{fun kapps_im:kvs_store/3, 'consumer_channel', kz_amqp_channel:consumer_channel()} + ,{fun kapps_im:set_controller_queue/2, ControllerQ} + ], + {'ok', #state{call = kapps_im:exec(Funs, Im) + ,queue = ControllerQ + }}. + +%%------------------------------------------------------------------------------ +%% @doc Handling call messages. +%% @end +%%------------------------------------------------------------------------------ +-spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_types:handle_call_ret_state(state()). +handle_call('queue_name', _From, #state{queue=Q}=State) -> + {'reply', Q, State}; +handle_call(_Request, _From, State) -> + lager:warning("unhandled request in call: ~p : ~p", [_Request, _From]), + Reply = {'error', 'unimplemented'}, + {'reply', Reply, State}. + +%%------------------------------------------------------------------------------ +%% @doc Handling cast messages. +%% @end +%%------------------------------------------------------------------------------ +-spec handle_cast(any(), state()) -> kz_types:handle_cast_ret_state(state()). +handle_cast({'continue', Key}, #state{flow=Flow}=State) -> + lager:info("continuing to child '~s'", [Key]), + + case kz_json:get_value([<<"children">>, Key], Flow) of + 'undefined' when Key =:= ?DEFAULT_CHILD_KEY -> + lager:info("wildcard child does not exist, we are lost... exiting"), + stop(self()), + {'noreply', State}; + 'undefined' -> + lager:info("requested child '~s' does not exist, trying wild card", [Key]), + continue(self()), + {'noreply', State}; + NewFlow -> + {'noreply', launch_tf_module(State#state{flow=NewFlow})} + end; +handle_cast({'stop', Cause}, State) -> + {'stop', {shutdown, Cause}, State#state{result=Cause}}; +handle_cast('initialize', State) -> + initialize(State); +handle_cast(_Msg, State) -> + lager:debug("unhandled cast: ~p", [_Msg]), + {'noreply', State}. + +%%------------------------------------------------------------------------------ +%% @doc Handling all non call/cast messages. +%% @end +%%------------------------------------------------------------------------------ +-spec handle_info(any(), state()) -> kz_types:handle_info_ret_state(state()). +handle_info({'amqp_msg', JObj}, State) -> + _ = handle_event(JObj, State), + {'noreply', State}; +handle_info({'kapi', {_, _, JObj}}, State) -> + _ = handle_event(JObj, State), + {'noreply', State}; +handle_info({'DOWN', Ref, 'process', Pid, 'normal'}, #state{tf_module_pid={Pid, Ref} + ,call=Im + }=State) -> + erlang:demonitor(Ref, ['flush']), + lager:debug("cf module ~s down normally", [kapps_im:kvs_fetch('tf_last_action', Im)]), + {'noreply', State#state{tf_module_pid='undefined'}}; +handle_info({'DOWN', Ref, 'process', Pid, 'killed'}, #state{tf_module_pid={Pid, Ref} + ,call=Im + }=State) -> + erlang:demonitor(Ref, ['flush']), + lager:debug("cf module ~s down normally", [kapps_im:kvs_fetch('tf_last_action', Im)]), + {'noreply', State#state{tf_module_pid='undefined'}}; +handle_info({'DOWN', Ref, 'process', Pid, _Reason}, #state{tf_module_pid={Pid, Ref} + ,call=Im + }=State) -> + erlang:demonitor(Ref, ['flush']), + LastAction = kapps_im:kvs_fetch('tf_last_action', Im), + lager:error("action ~s died unexpectedly: ~p", [LastAction, _Reason]), + continue(self()), + {'noreply', State#state{tf_module_pid='undefined'}}; +handle_info({'DOWN', _Ref, 'process', _Pid, 'normal'}, State) -> + {'noreply', State}; +handle_info({'EXIT', Pid, 'normal'}, #state{tf_module_pid={Pid, Ref} + ,call=Im + }=State) -> + erlang:demonitor(Ref, ['flush']), + lager:debug("cf module ~s down normally", [kapps_im:kvs_fetch('tf_last_action', Im)]), + {'noreply', State#state{tf_module_pid='undefined'}}; +handle_info({'EXIT', Pid, 'killed'}, #state{tf_module_pid={Pid, Ref} + ,call=Im + }=State) -> + erlang:demonitor(Ref, ['flush']), + lager:debug("cf module ~s killed normally", [kapps_im:kvs_fetch('tf_last_action', Im)]), + {'noreply', State#state{tf_module_pid='undefined'}}; +handle_info({'EXIT', Pid, _Reason}, #state{tf_module_pid={Pid, Ref} + ,call=Im + }=State) -> + erlang:demonitor(Ref, ['flush']), + LastAction = kapps_im:kvs_fetch('tf_last_action', Im), + lager:error_unsafe("action ~s died unexpectedly: ~p", [LastAction, _Reason]), + continue(self()), + {'noreply', State#state{tf_module_pid='undefined'}}; +handle_info({'EXIT', Pid, 'normal'}, #state{tf_module_old_pid={Pid, Ref} + ,call=Im + }=State) -> + erlang:demonitor(Ref, ['flush']), + lager:debug("cf module ~s down normally", [kapps_im:kvs_fetch('tf_old_action', Im)]), + {'noreply', State#state{tf_module_old_pid='undefined'}}; +handle_info({'EXIT', Pid, 'killed'}, #state{tf_module_old_pid={Pid, Ref} + ,call=Im + }=State) -> + erlang:demonitor(Ref, ['flush']), + lager:debug("cf module ~s killed normally", [kapps_im:kvs_fetch('tf_old_action', Im)]), + {'noreply', State#state{tf_module_old_pid='undefined'}}; +handle_info({'EXIT', Pid, _Reason}, #state{tf_module_old_pid={Pid, Ref} + ,call=Im + }=State) -> + erlang:demonitor(Ref, ['flush']), + LastAction = kapps_im:kvs_fetch('tf_old_action', Im), + lager:error("action ~s died unexpectedly: ~p", [LastAction, _Reason]), + {'noreply', State#state{tf_module_old_pid='undefined'}}; +handle_info({'amqp_return', _JObj, _Returned} = Msg, #state{tf_module_pid=PidRef + ,call=Im + } = State) -> + Others = kapps_im:kvs_fetch('tf_event_pids', [], Im), + Notify = case get_pid(PidRef) of + 'undefined' -> Others; + ModPid -> [ModPid | Others] + end, + relay_message(Notify, Msg), + {'noreply', State}; + +handle_info(_Msg, State) -> + lager:debug("unhandled message: ~p", [_Msg]), + {'noreply', State}. + +%%------------------------------------------------------------------------------ +%% @doc Handle call messages, sometimes forward them on. +%% @end +%%------------------------------------------------------------------------------ +-spec handle_event(kz_call_event:doc(), state()) -> 'ok'. +handle_event(JObj, #state{tf_module_pid=PidRef + ,call=Im + }) -> + Others = kapps_im:kvs_fetch('tf_event_pids', [], Im), + Notify = case get_pid(PidRef) of + 'undefined' -> Others; + ModPid -> [ModPid | Others] + end, + relay_message(Notify, JObj). + +%%------------------------------------------------------------------------------ +%% @doc This function is called by a `gen_server' when it is about to +%% terminate. It should be the opposite of `Module:init/1' and do any +%% necessary cleaning up. When it returns, the `gen_server' terminates +%% with Reason. The return value is ignored. +%% +%% @end +%%------------------------------------------------------------------------------ +-spec terminate(any(), state()) -> 'ok'. +terminate(_Reason, _State) -> 'ok'. + +%%------------------------------------------------------------------------------ +%% @doc Convert process state when code is changed. +%% @end +%%------------------------------------------------------------------------------ +-spec code_change(any(), state(), any()) -> {'ok', state()}. +code_change(_OldVsn, State, _Extra) -> + {'ok', State}. + +%%%============================================================================= +%%% Internal functions +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc This function determines if the callflow module specified at the +%% current node is 'available' and attempts to launch it if so. +%% Otherwise it will advance to the next child in the flow. +%% @end +%%------------------------------------------------------------------------------ +-spec launch_tf_module(state()) -> state(). +launch_tf_module(#state{flow=?EMPTY_JSON_OBJECT}=State) -> + lager:debug("no flow left to launch, maybe stopping"), + stop(self(), 'finish'), + State; +launch_tf_module(#state{flow=Flow + }=State) -> + do_launch_tf_module(State, find_module(Flow)). + +-spec do_launch_tf_module(state(), atom()) -> state(). +do_launch_tf_module(#state{call=Im + ,tf_module_pid=OldPidRef + }=State + ,'undefined' + ) -> + lager:error("unknown textflow action, reverting to last action"), + continue(self()), + OldAction = kapps_im:kvs_fetch('tf_last_action', Im), + State#state{tf_module_pid='undefined' + ,tf_module_old_pid=OldPidRef + ,call=update_actions(OldAction, Im) + }; +do_launch_tf_module(#state{call=Im + ,flow=Flow + ,tf_module_pid=OldPidRef + }=State + ,Action + ) -> + Data = kz_json:get_json_value(<<"data">>, Flow, kz_json:new()), + lager:info("moving to action '~s'", [Action]), + Im1 = update_actions(Action, Im), + PidRef = spawn_tf_module(Action, Data, Im1), + link(get_pid(PidRef)), + State#state{tf_module_pid=PidRef + ,tf_module_old_pid=OldPidRef + ,call=Im1 + }. + +-spec find_module(kz_json:object()) -> atom(). +find_module(Flow) -> + Module = kz_json:get_ne_binary_value(<<"module">>, Flow), + ModuleBin = <<"tf_", Module/binary>>, + Data = kz_json:get_json_value(<<"data">>, Flow, kz_json:new()), + TFModule = kz_term:to_atom(ModuleBin, 'true'), + IsExported = kz_module:is_exported(TFModule, 'handle', 2), + SkipModule = kz_json:is_true(<<"skip_module">>, Data, 'false'), + case IsExported + andalso (not SkipModule) + of + 'true' -> TFModule; + 'false' -> + lager:debug("skipping textflow module ~s (handle exported: ~s skip_module: ~s)" + ,[TFModule, IsExported, SkipModule]), + 'undefined' + end. + +-spec update_actions(atom(), kapps_im:im()) -> kapps_im:im(). +update_actions(Action, Im) -> + OldAction = kapps_im:kvs_fetch('tf_last_action', Im), + Routines = [{fun kapps_im:kvs_store/3, 'tf_old_action', OldAction} + ,{fun kapps_im:kvs_store/3, 'tf_last_action', Action} + ], + kapps_im:exec(Routines, Im). + +%%------------------------------------------------------------------------------ +%% @doc Helper function to spawn a linked callflow module, from the entry +%% point 'handle' having set the callid on the new process first. +%% @end +%%------------------------------------------------------------------------------ +-spec spawn_tf_module(atom(), kz_json:object(), kapps_im:im()) -> kz_term:pid_ref(). +spawn_tf_module(TFModule, Data, Im) -> + AMQPConsumer = kapps_im:kvs_fetch('consumer_pid', Im), + AMQPChannel = kapps_im:kvs_fetch('consumer_channel', Im), + kz_util:spawn_monitor(fun tf_module_task/5, [TFModule, Data, Im, AMQPConsumer, AMQPChannel]). + +-spec tf_module_task(atom(), kz_json:object(), kapps_im:im(), pid(), pid()) -> any(). +tf_module_task(TFModule, Data, Im, AMQPConsumer, AMQPChannel) -> + _ = kz_amqp_channel:consumer_channel(AMQPChannel), + _ = kz_amqp_channel:consumer_pid(AMQPConsumer), + kapps_im:put_message_id(Im), + try TFModule:handle(Data, Im) + catch + ?CATCH('exit', 'normal', _ST) -> + lager:info("action ~s finished", [TFModule]); + ?CATCH(_E, R, _ST) -> + lager:info("action ~s died unexpectedly (~s): ~p", [TFModule, _E, R]), + ?LOGSTACK(_ST), + throw(R) + end. + + +-spec log_call_information(kapps_im:im()) -> 'ok'. +log_call_information(Im) -> + lager:info("~s request ~s => ~s", [kapps_im:inception_type(Im), kapps_im:from_user(Im), kapps_im:to_user(Im)]). + +-spec relay_message(kz_term:pids(), kz_json:object() | {'amqp_return', kz_json:object(), kz_json:object()}) -> 'ok'. +relay_message(Notify, Message) -> + _ = [kapps_im_command:relay_event(Pid, Message) + || Pid <- Notify, + is_pid(Pid) + ], + 'ok'. + +-spec get_pid({pid(), reference()} | 'undefined') -> kz_term:api_pid(). +get_pid({Pid, _}) when is_pid(Pid) -> Pid; +get_pid(_) -> 'undefined'. + +-spec initialize(state()) -> {'stop', 'normal', state()} | + {'noreply', state()}. +initialize(#state{call=Im}=State) -> + log_call_information(Im), + Flow = kapps_im:kvs_fetch('tf_flow', Im), + {'noreply' + ,launch_tf_module(State#state{call=kapps_im:kvs_store('tf_exe_pid', self(), Im) + ,flow=Flow + }) + }. diff --git a/applications/doodle/src/doodle_shared_listener.erl b/applications/doodle/src/tf_exe_listener.erl similarity index 76% rename from applications/doodle/src/doodle_shared_listener.erl rename to applications/doodle/src/tf_exe_listener.erl index e7f84439010..742e63b04c5 100644 --- a/applications/doodle/src/doodle_shared_listener.erl +++ b/applications/doodle/src/tf_exe_listener.erl @@ -3,10 +3,13 @@ %%% @doc %%% @end %%%----------------------------------------------------------------------------- --module(doodle_shared_listener). +-module(tf_exe_listener). -behaviour(gen_listener). -export([start_link/0]). + +-export([tf_context/0]). + -export([init/1 ,handle_call/3 ,handle_cast/2 @@ -17,36 +20,29 @@ ]). -include("doodle.hrl"). --include_lib("kazoo_amqp/include/kapi_conf.hrl"). --include_lib("kazoo_documents/include/doc_types.hrl"). -define(SERVER, ?MODULE). --record(state, {}). --type state() :: #state{}. - --define(BINDINGS, [{'sms', [{'restrict_to', ['delivery','resume', 'outbound']}]} - ,{'registration', [{'restrict_to', ['reg_success']}]} - ,{'conf',[{'keys', [[{'action', 'created'}, {'doc_type', <<"sms">>}] - ,[{'doc_type', <<"device">>}] - ,[{'doc_type', <<"user">>}] - ]}]} - ,{'self', []} - ]). --define(RESPONDERS, [{'doodle_delivery_handler', [{<<"message">>, <<"delivery">>}]} - ,{'doodle_outbound_handler', [{<<"message">>, <<"outbound">>}]} - ,{'doodle_notify_handler', [{<<"directory">>, <<"reg_success">>}]} - ,{'doodle_doc_handler', [{<<"configuration">>, ?DOC_CREATED}]} - ,{'doodle_doc_handler', [{<<"configuration">>, ?DOC_EDITED}]} - ]). --define(QUEUE_NAME, <<"doodle_shared_listener">>). --define(QUEUE_OPTIONS, [{'exclusive', 'false'}]). --define(CONSUME_OPTIONS, [{'exclusive', 'false'}]). +-type state() :: #{}. + +-define(BINDINGS, [{'self', []}]). +-define(RESPONDERS, []). +-define(QUEUE_NAME, <<>>). +-define(QUEUE_OPTIONS, []). +-define(CONSUME_OPTIONS, []). %%%============================================================================= %%% API %%%============================================================================= +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec tf_context() -> map(). +tf_context() -> + gen_listener:call(?SERVER, 'tf_context'). + %%------------------------------------------------------------------------------ %% @doc Starts the server. %% @end @@ -74,13 +70,18 @@ start_link() -> %%------------------------------------------------------------------------------ -spec init([]) -> {'ok', state()}. init([]) -> - {'ok', #state{}}. + {'ok', #{}}. %%------------------------------------------------------------------------------ %% @doc Handling call messages. %% @end %%------------------------------------------------------------------------------ -spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_types:handle_call_ret_state(state()). +handle_call('tf_context', _From, #{queue := Queue} = State) -> + Reply = #{channel => kz_amqp_channel:consumer_channel() + ,queue => Queue + }, + {'reply', Reply, State}; handle_call(_Request, _From, State) -> {'reply', {'error', 'not_implemented'}, State}. @@ -89,8 +90,8 @@ handle_call(_Request, _From, State) -> %% @end %%------------------------------------------------------------------------------ -spec handle_cast(any(), state()) -> kz_types:handle_cast_ret_state(state()). -handle_cast({'gen_listener', {'created_queue', _QueueNAme}}, State) -> - {'noreply', State}; +handle_cast({'gen_listener', {'created_queue', Q}}, State) -> + {'noreply', State#{queue => Q}}; handle_cast({'gen_listener', {'is_consuming', _IsConsuming}}, State) -> {'noreply', State}; handle_cast(_Msg, State) -> diff --git a/applications/doodle/src/doodle_exe_sup.erl b/applications/doodle/src/tf_exe_sup.erl similarity index 88% rename from applications/doodle/src/doodle_exe_sup.erl rename to applications/doodle/src/tf_exe_sup.erl index 02b111e387c..8edbf1f958b 100644 --- a/applications/doodle/src/doodle_exe_sup.erl +++ b/applications/doodle/src/tf_exe_sup.erl @@ -4,7 +4,7 @@ %%% @author Karl Anderson %%% @end %%%----------------------------------------------------------------------------- --module(doodle_exe_sup). +-module(tf_exe_sup). -behaviour(supervisor). @@ -20,7 +20,7 @@ %% Supervisor callbacks -export([init/1]). --define(CHILDREN, [?WORKER_TYPE('doodle_exe', 'temporary')]). +-define(CHILDREN, [?WORKER_TYPE('tf_exe', 'temporary')]). %%============================================================================== %% API functions @@ -34,9 +34,10 @@ start_link() -> supervisor:start_link({'local', ?SERVER}, ?MODULE, []). --spec new(kapps_call:call()) -> kz_types:sup_startchild_ret(). -new(Call) -> - supervisor:start_child(?SERVER, [Call]). +-spec new(kapps_im:im()) -> kz_types:sup_startchild_ret(). +new(Im) -> + Context = tf_exe_listener:tf_context(), + supervisor:start_child(?SERVER, [Im, Context]). -spec workers() -> kz_term:pids(). workers() -> diff --git a/applications/doodle/src/tf_util.erl b/applications/doodle/src/tf_util.erl new file mode 100644 index 00000000000..841f03f4836 --- /dev/null +++ b/applications/doodle/src/tf_util.erl @@ -0,0 +1,125 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2011-2019, 2600Hz +%%% @doc +%%% @author Luis Azedo +%%% +%%% This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(tf_util). + +-include("doodle.hrl"). + +-export([build_im_endpoint/3 + ,delivery_status/1 + ]). + +-type build_error() :: 'endpoint_disabled' | 'do_not_disturb'. +-type delivery_status() :: 'delivered' | 'failed'. + +-export_type([build_error/0 + ,delivery_status/0 + ]). + +-spec build_im_endpoint(kz_json:object(), kz_json:object(), kapps_im:im()) -> {'ok', kz_json:objects()} | {'error', build_error()}. +build_im_endpoint(Endpoint, Properties, Im) -> + case should_create_endpoint(Endpoint, Properties, Im) of + 'ok' -> {'ok', create_im_endpoints(Endpoint, Properties, Im)}; + {'error', _}=E -> E + end. + +-spec should_create_endpoint(kz_json:object(), kz_json:object(), kapps_im:im()) -> 'ok' | {'error', build_error()}. +should_create_endpoint(Endpoint, Properties, Call) -> + case evaluate_rules_for_creation(Endpoint, Properties, Call) of + {Endpoint, Properties, Call} -> 'ok'; + {'error', _}=Error -> Error + end. + +-spec evaluate_rules_for_creation(kz_json:object(), kz_json:object(), kapps_im:im()) -> create_ep_acc(). +evaluate_rules_for_creation(Endpoint, Properties, Call) -> + Routines = [fun maybe_endpoint_disabled/3 + ,fun maybe_do_not_disturb/3 + ], + lists:foldl(fun should_create_endpoint_fold/2 + ,{Endpoint, Properties, Call} + ,Routines + ). + +-type create_ep_acc() :: {kz_json:object(), kz_json:object(), kapps_im:im()} | {'error', any()}. +-type ep_routine_v() :: fun((kz_json:object(), kz_json:object(), kapps_im:im()) -> 'ok' | _). + +-spec should_create_endpoint_fold(ep_routine_v(), create_ep_acc()) -> create_ep_acc(). +should_create_endpoint_fold(Routine, {Endpoint, Properties, Call}=Acc) + when is_function(Routine, 3) -> + try Routine(Endpoint, Properties, Call) of + 'ok' -> Acc; + Error -> Error + catch + ?CATCH('throw', Error, ST) -> + ?LOGSTACK(ST), + Error; + ?CATCH(_E, _R, ST) -> + lager:debug("exception ~p:~p", [_E, _R]), + ?LOGSTACK(ST), + {'error', 'exception'} + end; +should_create_endpoint_fold(_Routine, Error) -> Error. + +-spec maybe_endpoint_disabled(kz_json:object(), kz_json:object(), kapps_im:im()) -> 'ok' | {'error', 'endpoint_disabled'}. +maybe_endpoint_disabled(Endpoint, _Properties, _Im) -> + case kz_json:is_false(<<"enabled">>, Endpoint) of + 'false' -> 'ok'; + 'true' -> {'error', 'endpoint_disabled'} + end. + +-spec maybe_do_not_disturb(kz_json:object(), kz_json:object(), kapps_im:im()) -> 'ok' | {'error', 'do_not_disturb'}. +maybe_do_not_disturb(Endpoint, _Properties, _Im) -> + DND = kz_json:get_json_value(<<"do_not_disturb">>, Endpoint, kz_json:new()), + case kz_json:is_true(<<"enabled">>, DND) of + 'false' -> 'ok'; + 'true' -> {'error', 'do_not_disturb'} + end. + +-spec create_im_endpoints(kz_json:object(), kz_json:object(), kapps_im:im()) -> kz_json:objects(). +create_im_endpoints(Endpoint, Properties, Im) -> + create_im_endpoints(kz_doc:type(Endpoint), Endpoint, Properties, Im). + +-spec create_im_endpoints(kz_term:ne_binary(), kz_json:object(), kz_json:object(), kapps_im:im()) -> kz_json:objects(). +create_im_endpoints(<<"device">>, Endpoint, Properties, Im) -> + [create_im_endpoint(Endpoint, Properties, Im)]; +create_im_endpoints(<<"user">>, Endpoint, Properties, Im) -> + OwnerId = kz_doc:id(Endpoint), + EndpointIds = [kz_doc:id(EP) || EP + <- kz_attributes:owned_by_docs(OwnerId, kapps_im:account_id(Im)) + ,<<"device">> =:= kz_doc:type(EP) + ,<<"sip_device">> =:= kzd_devices:device_type(EP) + ], + EPs = [kz_endpoint:get(EndpointId, kapps_im:account_id(Im)) || EndpointId <- EndpointIds], + [create_im_endpoint(EP, Properties, Im) || {'ok', EP} <- EPs]; +create_im_endpoints(_, _Endpoint, _Properties, _Im) -> []. + +-spec create_im_endpoint(kz_json:object(), kz_json:object(), kapps_im:im()) -> kz_json:object(). +create_im_endpoint(Endpoint, _Properties, Im) -> + kz_json:from_list( + [{<<"To-Username">>, kzd_devices:sip_username(Endpoint)} + ,{<<"To-Realm">>, kzd_devices:sip_realm(Endpoint, kapps_im:to_realm(Im))} + ,{<<"To-DID">>, kapps_im:request_user(Im)} + ,{<<"Endpoint-ID">>, kz_doc:id(Endpoint)} + ,{<<"Invite-Format">>, kzd_devices:sip_invite_format(Endpoint)} + ]). + +-spec delivery_status(kz_term:api_object()) -> delivery_status(). +delivery_status(JObj) -> + DeliveryCode = kz_json:get_value(<<"Delivery-Result-Code">>, JObj), + Status = kz_json:get_value(<<"Status">>, JObj), + delivery_status(DeliveryCode, Status). + +-spec delivery_status(kz_term:api_binary(), kz_term:api_binary()) -> delivery_status(). +delivery_status(<<"sip:", Code/binary>>, Status) -> delivery_status(Code, Status); +delivery_status(<<"200">>, _) -> 'delivered'; +delivery_status(<<"202">>, _) -> 'delivered'; +delivery_status(_, <<"Success">>) -> 'delivered'; +delivery_status(_, _) -> 'failed'. diff --git a/applications/stepswitch/src/stepswitch_local_sms.erl b/applications/stepswitch/src/stepswitch_local_sms.erl deleted file mode 100644 index 6ea94e4e1af..00000000000 --- a/applications/stepswitch/src/stepswitch_local_sms.erl +++ /dev/null @@ -1,136 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @copyright (C) 2013-2019, 2600Hz -%%% @doc -%%% @end -%%%----------------------------------------------------------------------------- --module(stepswitch_local_sms). - --export([local_message_handling/2]). - --include("stepswitch.hrl"). - --spec local_message_handling(knm_number_options:extra_options(), kapi_offnet_resource:req()) -> 'ok'. -local_message_handling(Props, OffnetReq) -> - FetchId = kz_binary:rand_hex(16), - CallId = kz_binary:rand_hex(16), - ServerID = kapi_offnet_resource:server_id(OffnetReq), - ReqResp = kz_amqp_worker:call(route_req(CallId, FetchId, Props, OffnetReq) - ,fun kapi_route:publish_req/1 - ,fun kapi_route:is_actionable_resp/1 - ), - case ReqResp of - {'error', _R} -> - lager:info("did not receive route response for request ~s: ~p", [FetchId, _R]), - Delivery = delivery_from_req(OffnetReq, <<"Error">>, <<"500">>, 'true'), - send_sms_response(sms_error(Delivery, OffnetReq), ServerID); - {'ok', JObjResp} -> - 'true' = kapi_route:resp_v(JObjResp), - Delivery = delivery_from_req(OffnetReq, <<"Success">>, <<"200">>, 'undefined'), - send_sms_response(sms_success(Delivery, OffnetReq), ServerID), - send_route_win(FetchId, CallId, JObjResp) - end. - --spec sms_error(kz_json:object(), kapi_offnet_resource:req()) -> kz_term:proplist(). -sms_error(JObj, OffnetReq) -> - lager:debug("error during outbound request: ~s" - ,[kz_json:encode(kapi_offnet_resource:req_to_jobj(OffnetReq))] - ), - [{<<"Call-ID">>, kapi_offnet_resource:call_id(OffnetReq)} - ,{<<"Msg-ID">>, kapi_offnet_resource:msg_id(OffnetReq)} - ,{<<"Response-Message">>, <<"NORMAL_TEMPORARY_FAILURE">>} - ,{<<"Response-Code">>, <<"sip:500">>} - ,{<<"Error-Message">>, kz_json:get_value(<<"Error-Message">>, JObj, <<"failed to process request">>)} - ,{<<"To-DID">>, kapi_offnet_resource:to_did(OffnetReq)} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ]. - --spec sms_success(kz_json:object(), kapi_offnet_resource:req()) -> kz_term:proplist(). -sms_success(JObj, OffnetReq) -> - lager:debug("outbound request successfully completed"), - [{<<"Call-ID">>, kapi_offnet_resource:call_id(OffnetReq)} - ,{<<"Msg-ID">>, kapi_offnet_resource:msg_id(OffnetReq)} - ,{<<"Response-Message">>, <<"SUCCESS">>} - ,{<<"Response-Code">>, <<"sip:200">>} - ,{<<"Resource-Response">>, JObj} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ]. - --spec send_sms_response(kz_json:object() | kz_term:proplist(), kz_term:ne_binary()) -> 'ok'. -send_sms_response(JObj, ServerID) -> - kz_amqp_worker:cast(JObj, fun(A) -> kapi_offnet_resource:publish_resp(ServerID, A) end). - --spec send_route_win(kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object()) -> 'ok'. -send_route_win(_FetchId, CallId, JObj) -> - ServerQ = kz_api:server_id(JObj), - CCVs = kz_json:get_value(<<"Custom-Channel-Vars">>, JObj, kz_json:new()), - Win = [{<<"Msg-ID">>, CallId} - ,{<<"Call-ID">>, CallId} - ,{<<"Control-Queue">>, <<"chatplan_ignored">>} - ,{<<"Custom-Channel-Vars">>, CCVs} - | kz_api:default_headers(<<"dialplan">>, <<"route_win">>, ?APP_NAME, ?APP_VERSION) - ], - lager:debug("sending route_win to ~s", [ServerQ]), - kz_amqp_worker:cast(Win, fun(Payload)-> kapi_route:publish_win(ServerQ, Payload) end). - --spec delivery_from_req(kapi_offnet_resource:req(), binary(), kz_term:api_binary(), kz_term:api_boolean()) -> - kz_json:object(). -delivery_from_req(OffnetReq, Status, DeliveryCode, DeliveryFailure) -> - OffnetJObj = kapi_offnet_resource:req_to_jobj(OffnetReq), - Keys = [<<"Event-Category">> - ,<<"Event-Name">> - ,<<"App-Name">> - ,<<"App-Version">> - ,<<"Node">> - ], - Props = props:filter_empty( - [{<<"Delivery-Result-Code">>, DeliveryCode} - ,{<<"Delivery-Failure">>, DeliveryFailure} - ,{<<"Status">>, Status} - | kz_api:default_headers(<<"message">>, <<"delivery">>, ?APP_NAME, ?APP_VERSION) - ]), - - kz_json:set_values(Props - ,kz_json:delete_keys(Keys, OffnetJObj) - ). - --spec request_caller_id(kapi_offnet_resource:req()) -> {kz_term:ne_binary(), kz_term:ne_binary()}. -request_caller_id(OffnetReq) -> - AccountId = kapi_offnet_resource:account_id(OffnetReq), - {kapi_offnet_resource:outbound_caller_id_number(OffnetReq - ,kz_privacy:anonymous_caller_id_number(AccountId) - ) - ,kapi_offnet_resource:outbound_caller_id_name(OffnetReq - ,kapps_call:unknown_caller_id_name(AccountId) - ) - }. - --spec route_req(kz_term:ne_binary(), kz_term:ne_binary(), knm_number_options:extra_options(), kapi_offnet_resource:req()) -> kz_term:proplist(). -route_req(CallId, FetchId, Props, OffnetReq) -> - TargetAccountId = knm_number_options:account_id(Props), - TargetAccountRealm = kzd_accounts:fetch_realm(TargetAccountId), - OffnetReqAccountRealm = kapi_offnet_resource:account_realm(OffnetReq), - ToDID = kapi_offnet_resource:to_did(OffnetReq), - To = <>, - {FromNumber, FromName} = request_caller_id(OffnetReq), - From = <>, - - CCVs = kz_json:from_list( - [{<<"Fetch-ID">>, FetchId} - ,{<<"Account-ID">>, TargetAccountId} - ,{<<"Account-Realm">>, TargetAccountRealm} - ,{<<"Inception">>, From} - ]), - - [{<<"Msg-ID">>, FetchId} - ,{<<"Call-ID">>, CallId} - ,{<<"Message-ID">>, kapi_offnet_resource:message_id(OffnetReq)} - ,{<<"Caller-ID-Name">>, FromName} - ,{<<"Caller-ID-Number">>, FromNumber} - ,{<<"To">>, To} - ,{<<"From">>, From} - ,{<<"Request">>, To} - ,{<<"Body">>, kapi_offnet_resource:body(OffnetReq)} - ,{<<"Custom-Channel-Vars">>, CCVs} - ,{<<"Resource-Type">>, <<"sms">>} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ]. diff --git a/applications/stepswitch/src/stepswitch_outbound.erl b/applications/stepswitch/src/stepswitch_outbound.erl index 60773c80173..45bad295097 100644 --- a/applications/stepswitch/src/stepswitch_outbound.erl +++ b/applications/stepswitch/src/stepswitch_outbound.erl @@ -26,8 +26,7 @@ handle_req(OffnetJObj, _Props) -> _ = kapi_offnet_resource:put_callid(OffnetReq), case kapi_offnet_resource:resource_type(OffnetReq) of ?RESOURCE_TYPE_AUDIO -> handle_audio_req(OffnetReq); - ?RESOURCE_TYPE_ORIGINATE -> handle_originate_req(OffnetReq); - ?RESOURCE_TYPE_SMS -> handle_sms_req(OffnetReq) + ?RESOURCE_TYPE_ORIGINATE -> handle_originate_req(OffnetReq) end. %%------------------------------------------------------------------------------ @@ -83,20 +82,6 @@ maybe_force_originate_outbound(Props, OffnetReq) -> 'true' -> maybe_originate(knm_number_options:number(Props), OffnetReq) end. -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec handle_sms_req(kapi_offnet_resource:req()) -> any(). -handle_sms_req(OffnetReq) -> - Number = stepswitch_util:get_outbound_destination(OffnetReq), - lager:debug("received outbound sms resource request for ~s", [Number]), - case knm_number:lookup_account(Number) of - {'ok', _AccountId, Props} -> - maybe_force_outbound_sms(Props, OffnetReq); - _ -> maybe_sms(Number, OffnetReq) - end. - %%------------------------------------------------------------------------------ %% @doc %% @end @@ -111,21 +96,6 @@ maybe_force_outbound(Props, OffnetReq) -> 'true' -> maybe_bridge(knm_number_options:number(Props), OffnetReq) end. -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec maybe_force_outbound_sms(knm_number_options:extra_options(), kapi_offnet_resource:req()) -> any(). -maybe_force_outbound_sms(Props, OffnetReq) -> - case knm_number_options:should_force_outbound(Props) - orelse kapi_offnet_resource:force_outbound(OffnetReq, 'false') - orelse kapi_offnet_resource:hunt_account_id(OffnetReq) /= 'undefined' - orelse knm_number_options:assign_to(Props) =:= 'undefined' - of - 'false' -> local_sms(Props, OffnetReq); - 'true' -> maybe_sms(knm_number_options:number(Props), OffnetReq) - end. - %%------------------------------------------------------------------------------ %% @doc %% @end @@ -152,18 +122,6 @@ maybe_correct_shortdial(Number, OffnetReq) -> handle_audio_req(CorrectedNumber, OffnetReq) end. -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec maybe_sms(kz_term:ne_binary(), kapi_offnet_resource:req()) -> any(). -maybe_sms(Number, OffnetReq) -> - RouteBy = stepswitch_util:route_by(), - case RouteBy:endpoints(Number, OffnetReq) of - [] -> publish_no_resources(OffnetReq); - Endpoints -> stepswitch_request_sup:sms(Endpoints, OffnetReq) - end. - %%------------------------------------------------------------------------------ %% @doc %% @end @@ -173,14 +131,6 @@ maybe_sms(Number, OffnetReq) -> local_extension(Props, OffnetReq) -> stepswitch_request_sup:local_extension(Props, OffnetReq). -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec local_sms(knm_number_options:extra_options(), kapi_offnet_resource:req()) -> 'ok'. -local_sms(Props, OffnetReq) -> - stepswitch_local_sms:local_message_handling(Props, OffnetReq). - %%------------------------------------------------------------------------------ %% @doc %% @end diff --git a/applications/stepswitch/src/stepswitch_resources.erl b/applications/stepswitch/src/stepswitch_resources.erl index b9bfc885553..fd1352e7bf8 100644 --- a/applications/stepswitch/src/stepswitch_resources.erl +++ b/applications/stepswitch/src/stepswitch_resources.erl @@ -1241,21 +1241,6 @@ endpoint_options(JObj, <<"skype">>) -> [{<<"Skype-Interface">>, kz_json:get_value(<<"interface">>, JObj)} ,{<<"Skype-RR">>, kz_json:is_true(<<"skype_rr">>, JObj, true)} ]); -endpoint_options(JObj, <<"amqp">>) -> - Server = kz_json:get_value(<<"server">>, JObj), - User = kz_json:get_value(<<"username">>, JObj), - Password = kz_json:get_value(<<"password">>, JObj), - Broker = <<"amqp://", User/binary, ":", Password/binary, "@", Server/binary>>, - - kz_json:from_list( - [{<<"AMQP-Broker">>, Broker} - ,{<<"Exchange-ID">>, kz_json:get_value(<<"amqp_exchange">>, JObj)} - ,{<<"Exchange-Type">>, kz_json:get_value(<<"amqp_exchange_type">>, JObj)} - ,{<<"Route-ID">>, kz_json:get_value(<<"route_id">>, JObj)} - ,{<<"System-ID">>, kz_json:get_value(<<"system_id">>, JObj)} - ,{<<"Broker-Name">>, kz_json:get_value(<<"broker_name">>, JObj, kz_binary:rand_hex(6))} - ,{<<"Exchange-Options">>, kz_json:get_value(<<"amqp_exchange_options">>, JObj, ?DEFAULT_AMQP_EXCHANGE_OPTIONS)} - ]); endpoint_options(JObj, <<"sip">>) -> kz_json:from_list( [{<<"Route-ID">>, kz_json:get_value(<<"route_id">>, JObj)} diff --git a/applications/stepswitch/src/stepswitch_sms.erl b/applications/stepswitch/src/stepswitch_sms.erl deleted file mode 100644 index dbd522c83cc..00000000000 --- a/applications/stepswitch/src/stepswitch_sms.erl +++ /dev/null @@ -1,550 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @copyright (C) 2013-2019, 2600Hz -%%% @doc -%%% @end -%%%----------------------------------------------------------------------------- --module(stepswitch_sms). - --behaviour(gen_listener). - --export([start_link/2]). --export([init/1 - ,handle_call/3 - ,handle_cast/2 - ,handle_info/2 - ,handle_event/2 - ,terminate/2 - ,code_change/3 - ]). - --export([handle_message_delivery/2]). - --include("stepswitch.hrl"). - --define(SERVER, ?MODULE). - --record(state, {endpoints = [] :: kz_json:objects() - ,resource_req :: kapi_offnet_resource:req() - ,request_handler :: pid() - ,control_queue :: kz_term:api_binary() - ,response_queue :: kz_term:api_binary() - ,queue :: kz_term:api_binary() - ,message = [] :: kz_term:proplist() - ,messages = queue:new() :: queue:queue() - }). --type state() :: #state{}. - --define(RESPONDERS, [{{?MODULE, 'handle_message_delivery'} - ,[{<<"message">>, <<"delivery">>}] - } - ]). --define(QUEUE_NAME, <<>>). --define(QUEUE_OPTIONS, []). --define(CONSUME_OPTIONS, []). - --define(ATOM(X), kz_term:to_atom(X, 'true')). --define(SMS_POOL(A,B,C), ?ATOM(<>) ). - -%%%============================================================================= -%%% API -%%%============================================================================= - - -%%------------------------------------------------------------------------------ -%% @doc Starts the server. -%% @end -%%------------------------------------------------------------------------------ --spec start_link(kz_json:objects(), kapi_offnet_resource:req()) -> kz_types:startlink_ret(). -start_link(Endpoints, OffnetReq) -> - Bindings = [{'self', []}], - gen_listener:start_link(?SERVER, [{'bindings', Bindings} - ,{'responders', ?RESPONDERS} - ,{'queue_name', ?QUEUE_NAME} - ,{'queue_options', ?QUEUE_OPTIONS} - ,{'consume_options', ?CONSUME_OPTIONS} - ], [Endpoints, OffnetReq]). - -%%%============================================================================= -%%% gen_server callbacks -%%%============================================================================= - -%%------------------------------------------------------------------------------ -%% @doc Initializes the server. -%% @end -%%------------------------------------------------------------------------------ --spec init([kz_json:objects() | kapi_offnet_resource:req()]) -> {'ok', state()}. -init([Endpoints, OffnetReq]) -> - _ = kapi_offnet_resource:put_callid(OffnetReq), - CallId = kapi_offnet_resource:call_id(OffnetReq), - case kapi_offnet_resource:control_queue(OffnetReq) of - 'undefined' -> - lager:debug("Control-Queue is undefined for Call-ID ~s, exiting.", [CallId]), - {'stop', 'normal'}; - ControlQ -> - {'ok', #state{endpoints=Endpoints - ,resource_req=OffnetReq - ,request_handler=self() - ,control_queue=ControlQ - ,response_queue=kapi_offnet_resource:server_id(OffnetReq) - }} - end. - -%%------------------------------------------------------------------------------ -%% @doc Handling call messages. -%% @end -%%------------------------------------------------------------------------------ --spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_types:handle_call_ret_state(state()). -handle_call(_Request, _From, State) -> - lager:debug("unhandled call: ~p", [_Request]), - {'reply', {'error', 'not_implemented'}, State}. - -%%------------------------------------------------------------------------------ -%% @doc Handling cast messages. -%% @end -%%------------------------------------------------------------------------------ --spec handle_cast(any(), state()) -> kz_types:handle_cast_ret_state(state()). -handle_cast({'kz_amqp_channel', _}, State) -> - {'noreply', State}; -handle_cast({'gen_listener', {'created_queue', Q}}, State) -> - {'noreply', State#state{queue=Q}}; -handle_cast({'gen_listener', {'is_consuming', 'true'}}, State) -> - {'noreply', build_sms(State)}; -handle_cast({'sms_result', _Props}, #state{response_queue='undefined'}=State) -> - {'stop', 'normal', State}; -handle_cast({'sms_result', Props}, #state{response_queue=ResponseQ}=State) -> - kapi_offnet_resource:publish_resp(ResponseQ, Props), - {'stop', 'normal', State}; -handle_cast({'sms_success', JObj}, #state{resource_req=OffnetReq}=State) -> - gen_listener:cast(self(), {'sms_result', sms_success(JObj, OffnetReq)}), - {'noreply', State}; -handle_cast({'sms_failure', JObj}, #state{resource_req=OffnetReq}=State) -> - gen_listener:cast(self(), {'sms_result', sms_failure(JObj, OffnetReq)}), - {'noreply', State}; -handle_cast({'sms_error', JObj}, #state{resource_req=OffnetReq}=State) -> - gen_listener:cast(self(), {'sms_result', sms_error(JObj, OffnetReq)}), - {'noreply', State}; -handle_cast('next_message', #state{message=API - ,messages=Queue - ,resource_req=JObj - ,response_queue=ResponseQ - }=State) -> - case queue:out(Queue) of - {'empty', _} -> - kapi_offnet_resource:publish_resp(ResponseQ, sms_timeout(JObj)), - {'stop', 'normal', State}; - {{'value', Endpoint}, NewQ} -> - send(Endpoint, API), - {'noreply', State#state{messages=NewQ}} - end; -handle_cast(_Msg, State) -> - lager:debug("unhandled cast: ~p~n", [_Msg]), - {'noreply', State}. - -%%------------------------------------------------------------------------------ -%% @doc Handling all non call/cast messages. -%% @end -%%------------------------------------------------------------------------------ --spec handle_info(any(), state()) -> kz_types:handle_info_ret_state(state()). -handle_info('sms_timeout', State) -> - gen_listener:cast(self(), 'next_message'), - {'noreply', State}; -handle_info(_Info, State) -> - lager:debug("unhandled info: ~p", [_Info]), - {'noreply', State}. - -%%------------------------------------------------------------------------------ -%% @doc Allows listener to pass options to handlers. -%% @end -%%------------------------------------------------------------------------------ --spec handle_event(kz_json:object(), kz_term:proplist()) -> gen_listener:handle_event_return(). -handle_event(_JObj, _State) -> - {'reply', []}. - -%%------------------------------------------------------------------------------ -%% @doc This function is called by a `gen_server' when it is about to -%% terminate. It should be the opposite of `Module:init/1' and do any -%% necessary cleaning up. When it returns, the `gen_server' terminates -%% with Reason. The return value is ignored. -%% -%% @end -%%------------------------------------------------------------------------------ --spec terminate(any(), state()) -> 'ok'. -terminate(_Reason, _State) -> - lager:debug("listener terminating: ~p", [_Reason]). - -%%------------------------------------------------------------------------------ -%% @doc Convert process state when code is changed. -%% @end -%%------------------------------------------------------------------------------ --spec code_change(any(), state(), any()) -> {'ok', state()}. -code_change(_OldVsn, State, _Extra) -> - {'ok', State}. - -%%%============================================================================= -%%% Internal functions -%%%============================================================================= - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec handle_message_delivery(kz_json:object(), kz_term:proplist()) -> no_return(). -handle_message_delivery(JObj, Props) -> - _ = kz_util:put_callid(JObj), - Server = props:get_value('server',Props), - 'true' = kapi_sms:delivery_v(JObj), - case kz_json:is_true(<<"Delivery-Failure">>, JObj) of - 'true' -> gen_listener:cast(Server, {'sms_failure', JObj}); - 'false' -> gen_listener:cast(Server, {'sms_success', JObj}) - end. - --spec send(kz_json:object(), kz_term:proplist()) -> no_return(). -send(Endpoint, API) -> - Type = kz_json:get_value(<<"Endpoint-Type">>, Endpoint, <<"sip">>), - send(Type, Endpoint, API). - --spec send(binary(), kz_json:object(), kz_term:proplist()) -> no_return(). -send(<<"sip">>, Endpoint, API) -> - Options = kz_json:to_proplist(kz_json:get_value(<<"Endpoint-Options">>, Endpoint, [])), - Payload = props:set_values([{<<"Endpoints">>, [Endpoint]} - | Options - ] - ,API - ), - CallId = props:get_value(<<"Call-ID">>, Payload), - lager:debug("sending sms and waiting for response ~s", [CallId]), - _ = kz_amqp_worker:cast(Payload, fun kapi_sms:publish_message/1), - erlang:send_after(60000, self(), 'sms_timeout'); -send(<<"amqp">>, Endpoint, API) -> - CallId = props:get_value(<<"Call-ID">>, API), - Options = kz_json:to_proplist(kz_json:get_value(<<"Endpoint-Options">>, Endpoint, [])), - CCVs = kz_json:merge_jobjs(kz_json:get_json_value(<<"Custom-Channel-Vars">>, Endpoint, kz_json:new()) - ,kz_json:filter(fun filter_smpp/1, props:get_value(<<"Custom-Channel-Vars">>, API, kz_json:new())) - ), - Props = kz_json:to_proplist(Endpoint) ++ Options, - Payload = props:set_value(<<"Custom-Channel-Vars">>, CCVs, props:set_values(Props, API)), - Broker = kz_json:get_value([<<"Endpoint-Options">>, <<"AMQP-Broker">>], Endpoint), - BrokerName = kz_json:get_value([<<"Endpoint-Options">>, <<"Broker-Name">>], Endpoint), - Exchange = kz_json:get_value([<<"Endpoint-Options">>, <<"Exchange-ID">>], Endpoint), - RouteId = kz_json:get_value([<<"Endpoint-Options">>, <<"Route-ID">>], Endpoint), - ExchangeType = kz_json:get_value([<<"Endpoint-Options">>, <<"Exchange-Type">>], Endpoint, <<"topic">>), - ExchangeOptions = amqp_exchange_options(kz_json:get_value([<<"Endpoint-Options">>, <<"Exchange-Options">>], Endpoint)), - maybe_add_broker(Broker, Exchange, RouteId, ExchangeType, ExchangeOptions, BrokerName), - - lager:debug("sending sms ~s/~s and not waiting for response ~s", [Exchange, RouteId, CallId]), - case send_amqp_sms(Payload, ?SMS_POOL(Exchange, RouteId, BrokerName)) of - 'ok' -> - send_success(API, CallId); - {'error', 'timeout'} -> - send_timeout_error(API, CallId); - {'error', Reason} -> - send_error(API, CallId, Reason) - end. - --spec filter_smpp({kz_term:ne_binary(), any()}) -> boolean(). -filter_smpp({<<"SMPP-", _/binary>>, _}) -> 'true'; -filter_smpp(_) -> 'false'. - --spec send_success(kz_term:proplist(), kz_term:ne_binary()) -> 'ok'. -send_success(API, CallId) -> - DeliveryProps = [{<<"Delivery-Result-Code">>, <<"sip:200">>} - ,{<<"Status">>, <<"Success">>} - ,{<<"Message-ID">>, props:get_value(<<"Message-ID">>, API)} - ,{<<"Call-ID">>, CallId} - | kz_api:default_headers(<<"message">>, <<"delivery">>, ?APP_NAME, ?APP_VERSION) - ], - gen_listener:cast(self(), {'sms_success', kz_json:from_list(DeliveryProps)}). - --spec send_timeout_error(kz_term:proplist(), kz_term:ne_binary()) -> 'ok'. -send_timeout_error(API, CallId) -> - DeliveryProps = [{<<"Delivery-Result-Code">>, <<"sip:500">>} - ,{<<"Delivery-Failure">>, 'true'} - ,{<<"Error-Code">>, 500} - ,{<<"Error-Message">>, <<"timeout">>} - ,{<<"Status">>, <<"Failed">>} - ,{<<"Message-ID">>, props:get_value(<<"Message-ID">>, API)} - ,{<<"Call-ID">>, CallId} - | kz_api:default_headers(<<"message">>, <<"delivery">>, ?APP_NAME, ?APP_VERSION) - ], - gen_listener:cast(self(), {'sms_error', kz_json:from_list(DeliveryProps)}). - --spec send_error(kz_term:proplist(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -send_error(API, CallId, Reason) -> - DeliveryProps = [{<<"Delivery-Result-Code">>, <<"sip:500">>} - ,{<<"Delivery-Failure">>, 'true'} - ,{<<"Error-Code">>, 500} - ,{<<"Error-Message">>, kz_term:error_to_binary(Reason)} - ,{<<"Status">>, <<"Failed">>} - ,{<<"Message-ID">>, props:get_value(<<"Message-ID">>, API)} - ,{<<"Call-ID">>, CallId} - | kz_api:default_headers(<<"message">>, <<"delivery">>, ?APP_NAME, ?APP_VERSION) - ], - gen_listener:cast(self(), {'sms_error', kz_json:from_list(DeliveryProps)}). - --spec amqp_exchange_options(kz_term:api_object()) -> kz_term:proplist(). -amqp_exchange_options('undefined') -> []; -amqp_exchange_options(JObj) -> - [{kz_term:to_atom(K, 'true'), V} - || {K, V} <- kz_json:to_proplist(JObj) - ]. - --spec send_amqp_sms(kz_term:proplist(), atom()) -> - 'ok' | - {'error', kz_term:ne_binary() | 'timeout'}. -send_amqp_sms(Payload, Pool) -> - kapps_sms_command:send_amqp_sms(Payload, Pool). - --spec maybe_add_broker(kz_term:api_binary(), kz_term:api_binary(), kz_term:api_binary(), kz_term:ne_binary(), kz_term:proplist(), kz_term:api_binary()) -> 'ok'. -maybe_add_broker(Broker, Exchange, RouteId, ExchangeType, ExchangeOptions, BrokerName) -> - PoolExists = kz_amqp_sup:pool_pid(?SMS_POOL(Exchange, RouteId, BrokerName)) =/= 'undefined', - maybe_add_broker(Broker, Exchange, RouteId, ExchangeType, ExchangeOptions, BrokerName, PoolExists). - --spec maybe_add_broker(kz_term:api_binary(), kz_term:api_binary(), kz_term:api_binary(), kz_term:ne_binary(), kz_term:proplist(), kz_term:api_binary(), boolean()) -> 'ok'. -maybe_add_broker(_Broker, _Exchange, _RouteId, _ExchangeType, _ExchangeOptions, _BrokerName, 'true') -> 'ok'; -maybe_add_broker(Broker, Exchange, RouteId, ExchangeType, ExchangeOptions, BrokerName, 'false') -> - Exchanges = [{Exchange, ExchangeType, ExchangeOptions}], - _ = kz_amqp_sup:add_amqp_pool(?SMS_POOL(Exchange, RouteId, BrokerName), Broker, 5, 5, [], Exchanges, 'true'), - 'ok'. - --spec build_sms(state()) -> state(). -build_sms(#state{endpoints=Endpoints - ,resource_req=OffnetReq - ,queue=Q - }=State) -> - {CIDNum, CIDName} = bridge_caller_id(Endpoints, OffnetReq), - gen_listener:cast(self(), 'next_message'), - State#state{messages=queue:from_list(maybe_endpoints_format_from(Endpoints, CIDNum, OffnetReq)) - ,message=build_sms_base({CIDNum, CIDName}, OffnetReq, Q) - }. - --spec build_sms_base({binary(), binary()}, kapi_offnet_resource:req(), binary()) -> kz_term:proplist(). -build_sms_base({CIDNum, CIDName}, OffnetReq, Q) -> - AccountId = kapi_offnet_resource:account_id(OffnetReq), - AccountRealm = kapi_offnet_resource:account_realm(OffnetReq), - CCVs = kapi_offnet_resource:custom_channel_vars(OffnetReq, kz_json:new()), - CCVUpdates = props:filter_undefined( - [{<<"Ignore-Display-Updates">>, <<"true">>} - ,{<<"Account-ID">>, AccountId} - ,{<<"Account-Realm">>, AccountRealm} - ,{<<"From-URI">>, bridge_from_uri(CIDNum, OffnetReq)} - ,{<<"Reseller-ID">>, kz_services_reseller:get_id(AccountId)} - ]), - props:filter_undefined( - [{<<"Application-Name">>, <<"send">>} - ,{<<"Dial-Endpoint-Method">>, <<"single">>} - ,{<<"Outbound-Caller-ID-Number">>, CIDNum} - ,{<<"Outbound-Caller-ID-Name">>, CIDName} - ,{<<"Caller-ID-Number">>, CIDNum} - ,{<<"Caller-ID-Name">>, CIDName} - ,{<<"Presence-ID">>, kapi_offnet_resource:presence_id(OffnetReq)} - ,{<<"Custom-Channel-Vars">>, kz_json:set_values(CCVUpdates, CCVs)} - ,{<<"Custom-SIP-Headers">>, stepswitch_util:get_sip_headers(OffnetReq)} - ,{<<"Call-ID">>, kapi_offnet_resource:call_id(OffnetReq)} - ,{<<"Outbound-Callee-ID-Number">>, kapi_offnet_resource:outbound_callee_id_number(OffnetReq)} - ,{<<"Outbound-Callee-ID-Name">>, kapi_offnet_resource:outbound_callee_id_name(OffnetReq)} - ,{<<"Callee-ID-Number">>, kapi_offnet_resource:to_did(OffnetReq)} - ,{<<"Message-ID">>, kapi_offnet_resource:message_id(OffnetReq)} - ,{<<"Body">>, kapi_offnet_resource:body(OffnetReq)} - | kz_api:default_headers(Q, ?APP_NAME, ?APP_VERSION) - ]). - --spec maybe_endpoints_format_from(kz_json:objects(), kz_term:api_binary(), kapi_offnet_resource:req()) -> - kz_json:objects(). -maybe_endpoints_format_from([], _ , _) -> []; -maybe_endpoints_format_from(Endpoints, 'undefined', _) -> Endpoints; -maybe_endpoints_format_from(Endpoints, CIDNum, OffnetReq) -> - DefaultRealm = stepswitch_util:default_realm(OffnetReq), - [maybe_endpoint_format_from(Endpoint, CIDNum, DefaultRealm) - || Endpoint <- Endpoints - ]. - --spec maybe_endpoint_format_from(kz_json:object(), kz_term:ne_binary(), kz_term:api_binary()) -> - kz_json:object(). -maybe_endpoint_format_from(Endpoint, CIDNum, DefaultRealm) -> - CCVs = kz_json:get_json_value(<<"Custom-Channel-Vars">>, Endpoint, kz_json:new()), - case kz_json:is_true(<<"Format-From-URI">>, CCVs) of - 'true' -> endpoint_format_from(Endpoint, CIDNum, DefaultRealm); - 'false' -> - kz_json:set_value(<<"Custom-Channel-Vars">> - ,kz_json:delete_keys([<<"Format-From-URI">> - ,<<"From-URI-Realm">> - ] - ,CCVs - ) - ,Endpoint - ) - end. - --spec endpoint_format_from(kz_json:object(), kz_term:ne_binary(), kz_term:api_binary()) -> kz_json:object(). -endpoint_format_from(Endpoint, CIDNum, DefaultRealm) -> - CCVs = kz_json:get_value(<<"Custom-Channel-Vars">>, Endpoint, kz_json:new()), - Realm = kz_json:get_value(<<"From-URI-Realm">>, CCVs, DefaultRealm), - case is_binary(Realm) of - 'false' -> - kz_json:set_value(<<"Custom-Channel-Vars">> - ,kz_json:delete_keys([<<"Format-From-URI">> - ,<<"From-URI-Realm">> - ] - ,CCVs - ) - ,Endpoint - ); - 'true' -> - FromURI = <<"sip:", CIDNum/binary, "@", Realm/binary>>, - lager:debug("setting resource ~s from-uri to ~s" - ,[kz_json:get_value(<<"Resource-ID">>, CCVs) - ,FromURI - ]), - UpdatedCCVs = kz_json:set_value(<<"From-URI">>, FromURI, CCVs), - kz_json:set_value(<<"Custom-Channel-Vars">> - ,kz_json:delete_keys([<<"Format-From-URI">> - ,<<"From-URI-Realm">> - ] - ,UpdatedCCVs - ) - ,Endpoint - ) - end. - --spec bridge_caller_id(kz_json:objects(), kapi_offnet_resource:req()) -> - {kz_term:api_binary(), kz_term:api_binary()}. -bridge_caller_id(Endpoints, JObj) -> - case contains_emergency_endpoints(Endpoints) of - 'true' -> bridge_emergency_caller_id(JObj); - 'false' -> bridge_caller_id(JObj) - end. - --spec bridge_emergency_caller_id(kapi_offnet_resource:req()) -> - {kz_term:api_binary(), kz_term:api_binary()}. -bridge_emergency_caller_id(OffnetReq) -> - lager:debug("outbound call is using an emergency route, attempting to set CID accordingly"), - {maybe_emergency_cid_number(OffnetReq) - ,stepswitch_bridge:bridge_emergency_cid_name(OffnetReq) - }. - --spec bridge_caller_id(kapi_offnet_resource:req()) -> - {kz_term:ne_binary(), kz_term:ne_binary()}. -bridge_caller_id(OffnetReq) -> - {stepswitch_bridge:bridge_outbound_cid_number(OffnetReq) - ,stepswitch_bridge:bridge_outbound_cid_name(OffnetReq) - }. - --spec bridge_from_uri(kz_term:api_binary(), kapi_offnet_resource:req()) -> - kz_term:api_binary(). -bridge_from_uri(CIDNum, OffnetReq) -> - Realm = stepswitch_util:default_realm(OffnetReq), - case (kapps_config:get_is_true(?APP_NAME, <<"format_from_uri">>, 'false') - orelse kapi_offnet_resource:format_from_uri(OffnetReq) - ) - andalso (is_binary(CIDNum) - andalso is_binary(Realm) - ) - of - 'false' -> 'undefined'; - 'true' -> - FromURI = <<"sip:", CIDNum/binary, "@", Realm/binary>>, - lager:debug("setting bridge from-uri to ~s", [FromURI]), - FromURI - end. - --spec maybe_emergency_cid_number(kapi_offnet_resource:req()) -> - kz_term:api_binary(). -maybe_emergency_cid_number(OffnetReq) -> - %% NOTE: if this request had a hunt-account-id then we - %% are assuming it was for a local resource (at the - %% time of this commit offnet DB is still in use) - case kapi_offnet_resource:hunt_account_id(OffnetReq) of - 'undefined' -> emergency_cid_number(OffnetReq); - _Else -> - stepswitch_bridge:bridge_emergency_cid_number(OffnetReq) - end. - --spec emergency_cid_number(kapi_offnet_resource:req()) -> kz_term:ne_binary(). -emergency_cid_number(OffnetReq) -> - AccountId = kapi_offnet_resource:account_id(OffnetReq), - Candidates = [kapi_offnet_resource:emergency_caller_id_number(OffnetReq) - ,kapi_offnet_resource:outbound_caller_id_number(OffnetReq) - ], - Requested = stepswitch_bridge:bridge_emergency_cid_number(OffnetReq), - lager:debug("ensuring requested CID is emergency enabled: ~s", [Requested]), - EnabledNumbers = knm_numbers:emergency_enabled(AccountId), - emergency_cid_number(Requested, Candidates, EnabledNumbers). - --spec emergency_cid_number(kz_term:ne_binary(), kz_term:api_binaries(), kz_term:ne_binaries()) -> kz_term:ne_binary(). -%% if there are no emergency enabled numbers then either use the global system default -%% or the requested (if there isn't one) -emergency_cid_number(Requested, _, []) -> - case ?DEFAULT_EMERGENCY_CID_NUMBER of - 'undefined' -> Requested; - DefaultEmergencyCID -> DefaultEmergencyCID - end; -%% If neither their emergency cid or outgoing cid is emergency enabled but their account -%% has other numbers with emergency then use the first... -emergency_cid_number(_, [], [EmergencyEnabled|_]) -> EmergencyEnabled; -%% due to the way we built the candidates list it can contain the atom 'undefined' -%% handle that condition (ignore) -emergency_cid_number(Requested, ['undefined'|Candidates], EmergencyEnabled) -> - emergency_cid_number(Requested, Candidates, EmergencyEnabled); -%% check if the first non-atom undefined element in the list is in the list of -%% emergency enabled numbers, if so use it otherwise keep checking. -emergency_cid_number(Requested, [Candidate|Candidates], EmergencyEnabled) -> - case lists:member(Candidate, EmergencyEnabled) of - 'true' -> Candidate; - 'false' -> emergency_cid_number(Requested, Candidates, EmergencyEnabled) - end. - --spec contains_emergency_endpoints(kz_json:objects()) -> boolean(). -contains_emergency_endpoints([]) -> 'false'; -contains_emergency_endpoints([Endpoint|Endpoints]) -> - kz_json:is_true([<<"Custom-Channel-Vars">>, <<"Emergency-Resource">>], Endpoint) - orelse contains_emergency_endpoints(Endpoints). - --spec sms_timeout(kapi_offnet_resource:req()) -> kz_term:proplist(). -sms_timeout(OffnetReq) -> - lager:debug("attempt to connect to resources timed out"), - [{<<"Call-ID">>, kapi_offnet_resource:call_id(OffnetReq)} - ,{<<"Msg-ID">>, kapi_offnet_resource:msg_id(OffnetReq)} - ,{<<"Response-Message">>, <<"NORMAL_TEMPORARY_FAILURE">>} - ,{<<"Response-Code">>, <<"sip:500">>} - ,{<<"Error-Message">>, <<"bridge request timed out">>} - ,{<<"To-DID">>, kapi_offnet_resource:to_did(OffnetReq)} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ]. - --spec sms_error(kz_json:object(), kapi_offnet_resource:req()) -> kz_term:proplist(). -sms_error(JObj, OffnetReq) -> - lager:debug("error during outbound request: ~s", [kz_term:to_binary(kz_json:encode(JObj))]), - [{<<"Call-ID">>, kapi_offnet_resource:call_id(OffnetReq)} - ,{<<"Msg-ID">>, kapi_offnet_resource:msg_id(OffnetReq)} - ,{<<"Response-Message">>, <<"NORMAL_TEMPORARY_FAILURE">>} - ,{<<"Response-Code">>, <<"sip:500">>} - ,{<<"Error-Message">>, kz_json:get_value(<<"Error-Message">>, JObj, <<"failed to process request">>)} - ,{<<"To-DID">>, kapi_offnet_resource:to_did(OffnetReq)} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ]. - --spec sms_success(kz_json:object(), kapi_offnet_resource:req()) -> kz_term:proplist(). -sms_success(JObj, OffnetReq) -> - lager:debug("outbound request successfully completed"), - [{<<"Call-ID">>, kapi_offnet_resource:call_id(OffnetReq)} - ,{<<"Msg-ID">>, kapi_offnet_resource:msg_id(OffnetReq)} - ,{<<"Response-Message">>, <<"SUCCESS">>} - ,{<<"Response-Code">>, <<"sip:200">>} - ,{<<"Resource-Response">>, JObj} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ]. - --spec sms_failure(kz_json:object(), kapi_offnet_resource:req()) -> kz_term:proplist(). -sms_failure(JObj, OffnetReq) -> - lager:debug("resources for outbound request failed: ~s" - ,[kz_json:get_value(<<"Disposition">>, JObj)] - ), - [{<<"Call-ID">>, kapi_offnet_resource:call_id(OffnetReq)} - ,{<<"Msg-ID">>, kapi_offnet_resource:msg_id(OffnetReq)} - ,{<<"Response-Message">>, kz_json:get_first_defined([<<"Application-Response">> - ,<<"Hangup-Cause">> - ], JObj)} - ,{<<"Response-Code">>, kz_json:get_value(<<"Hangup-Code">>, JObj)} - ,{<<"Resource-Response">>, JObj} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ]. diff --git a/applications/webhooks/src/modules/webhooks_sms.erl b/applications/webhooks/src/modules/webhooks_sms.erl index 5737f62f37c..46d2d3ffe9a 100644 --- a/applications/webhooks/src/modules/webhooks_sms.erl +++ b/applications/webhooks/src/modules/webhooks_sms.erl @@ -12,13 +12,11 @@ ]). -include("webhooks.hrl"). --include_lib("kazoo_amqp/include/kapi_conf.hrl"). --include_lib("kazoo_documents/include/doc_types.hrl"). -define(ID, kz_term:to_binary(?MODULE)). -define(HOOK_NAME, <<"sms">>). -define(NAME, <<"SMS">>). --define(DESC, <<"Receive notifications when sms is created">>). +-define(DESC, <<"Receive notifications when sms is received">>). -define(METADATA ,kz_json:from_list( @@ -44,7 +42,7 @@ init() -> -spec bindings_and_responders() -> {gen_listener:bindings(), gen_listener:responders()}. bindings_and_responders() -> Bindings = bindings(), - Responders = [{{?MODULE, 'handle_sms'}, [{<<"configuration">>, ?DOC_CREATED}]}], + Responders = [{{?MODULE, 'handle_sms'}, [{<<"message">>, <<"inbound">>}]}], {Bindings, Responders}. %%------------------------------------------------------------------------------ @@ -59,32 +57,17 @@ account_bindings(_AccountId) -> []. %% @end %%------------------------------------------------------------------------------ -spec handle_sms(kz_json:object(), kz_term:proplist()) -> any(). -handle_sms(JObj, _Props) -> - kz_util:put_callid(JObj), - 'true' = kapi_conf:doc_update_v(JObj), - Type = kapi_conf:get_type(JObj), - Action = kz_api:event_name(JObj), - handle_sms(Action, Type, JObj). - --spec handle_sms(kz_term:api_ne_binary(), kz_term:api_ne_binary(), json:object()) -> any(). -handle_sms(?DOC_CREATED, <<"sms">>, JObj) -> - Db = kapi_conf:get_database(JObj), - Id = kapi_conf:get_id(JObj), - {'ok', Doc} = kz_datamgr:open_doc(Db, Id), - AccountId = kz_util:format_account_id(Db), - case AccountId =/= 'undefined' - andalso webhooks_util:find_webhooks(?HOOK_NAME, AccountId) of - 'false' -> 'ok'; +handle_sms(Payload, _Props) -> + kz_util:put_callid(Payload), + 'true' = kapi_sms:inbound_v(Payload), + AccountId = kz_api_sms:account_id(Payload), + case webhooks_util:find_webhooks(?HOOK_NAME, AccountId) of [] -> - lager:debug("no hooks to handle ~s for ~s" - ,[kz_api:event_name(JObj), AccountId] - ); + lager:debug("no hooks to handle ~s for ~s", [kz_api:event_name(Payload), AccountId]); Hooks -> - Event = format_event(Doc, AccountId), + Event = format_event(Payload, AccountId), webhooks_util:fire_hooks(Event, Hooks) - end; -handle_sms(_Action, _Type, _JObj) -> - 'ok'. + end. %%%============================================================================= %%% Internal functions @@ -97,22 +80,22 @@ handle_sms(_Action, _Type, _JObj) -> %%------------------------------------------------------------------------------ -spec bindings() -> gen_listener:bindings(). bindings() -> - [{'conf', [{'restrict_to', ['doc_updates']} - ,{'action', 'created'} - ,{'doc_type', <<"sms">>} - ]}]. + [{'sms', [{'restrict_to', ['inbound']} + ,{'route_type', 'offnet'} + ] + } + ]. %%------------------------------------------------------------------------------ %% @doc %% @end %%------------------------------------------------------------------------------ --spec format_event(kz_json:object(), kz_term:ne_binary()) -> kz_json:object(). -format_event(JObj, AccountId) -> +-spec format_event(kz_api_sms:payload(), kz_term:ne_binary()) -> kz_json:object(). +format_event(Payload, AccountId) -> kz_json:from_list( - [{<<"id">>, kz_doc:id(JObj)} + [{<<"id">>, kz_doc:id(Payload, kz_api_sms:message_id(Payload))} ,{<<"account_id">>, AccountId} - ,{<<"from">>, kzd_sms:from_user(JObj)} - ,{<<"to">>, kzd_sms:to_user(JObj)} - ,{<<"body">>, kzd_sms:body(JObj)} - ,{<<"direction">>, kzd_sms:direction(JObj)} - ,{<<"status">>, kzd_sms:status(JObj)} + ,{<<"from">>, kz_api_sms:from(Payload)} + ,{<<"to">>, kz_api_sms:to(Payload)} + ,{<<"body">>, kz_api_sms:body(Payload)} + ,{<<"origin">>, kz_api_sms:route_type(Payload)} ]). diff --git a/core/kazoo/include/kz_api_literals.hrl b/core/kazoo/include/kz_api_literals.hrl index 9fa74bc8179..d00718a1db5 100644 --- a/core/kazoo/include/kz_api_literals.hrl +++ b/core/kazoo/include/kz_api_literals.hrl @@ -27,6 +27,7 @@ -define(KEY_REQUEST_FROM_PID, <<"Request-From-PID">>). -define(KEY_REPLY_TO_PID, <<"Reply-To-PID">>). +-define(KEY_DELIVER_TO_PID, <<"Deliver-To-PID">>). -define(KZ_API_LITERALS_HRL, 'true'). -endif. diff --git a/core/kazoo_amqp/include/kz_amqp.hrl b/core/kazoo_amqp/include/kz_amqp.hrl index 542f0cce54c..b882a8ccc50 100644 --- a/core/kazoo_amqp/include/kz_amqp.hrl +++ b/core/kazoo_amqp/include/kz_amqp.hrl @@ -7,6 +7,7 @@ -ifndef(KZ_AMQP_HRL). -include_lib("amqp_client/include/amqp_client.hrl"). +-include("kz_api.hrl"). -define(KEY_ORGN_RESOURCE_REQ, <<"orginate.resource.req">>). %% corresponds to originate_resource_req/1 api call -define(RESOURCE_QUEUE_NAME, <<"resource.provider">>). diff --git a/core/kazoo_amqp/src/api/kapi.erl b/core/kazoo_amqp/src/api/kapi.erl new file mode 100644 index 00000000000..6a3fc48f9f2 --- /dev/null +++ b/core/kazoo_amqp/src/api/kapi.erl @@ -0,0 +1,69 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2010-2019, 2600Hz +%%% @doc Kazoo API Helpers. +%%% Most API functions take a proplist, filter it against required headers +%%% and optional headers, and return either the JSON string if all +%%% required headers (default AND API-call-specific) are present, or an +%%% error if some headers are missing. +%%% +%%% To only check the validity, use the API call's corresponding *_v/1 function. +%%% This will parse the proplist and return a boolean() if the proplist is valid +%%% for creating a JSON message. +%%% +%%% +%%% @author James Aimonetti +%%% @author Karl Anderson +%%% This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(kapi). + +%% API +-export([delivery_message/2 + ,encode_pid/1, encode_pid/2 + ,decode_pid/1 + ]). + +-include_lib("kz_amqp_util.hrl"). + +-spec event_category(kz_json:object()) -> atom(). +event_category(JObj) -> + kz_term:to_atom(kz_api:event_category(JObj), 'true'). + +-spec event_name(kz_json:object()) -> atom(). +event_name(JObj) -> + kz_term:to_atom(kz_api:event_name(JObj), 'true'). + +-spec delivery_message(JObj, kz_term:proplist()) -> + {{kz_term:ne_binary(), kz_term:ne_binary(), {#'P_basic'{}, #'basic.deliver'{}}} + ,{atom(), atom()} + ,JObj + } + when JObj :: kz_json:object(). +delivery_message(JObj, Props) -> + Basic = props:get_value('basic', Props), + Deliver = #'basic.deliver'{exchange=Exchange, routing_key=RK} = props:get_value('deliver', Props), + {{Exchange, RK, {Basic, Deliver}} + ,{event_category(JObj), event_name(JObj)} + ,JObj + }. + +-spec encode_pid(kz_term:ne_binary()) -> kz_term:ne_binary(). +encode_pid(Queue) -> + encode_pid(Queue, self()). + +-spec encode_pid(kz_term:ne_binary(), pid()) -> kz_term:ne_binary(). +encode_pid(Queue, Pid) -> + list_to_binary(["pid://", kz_term:to_binary(Pid), "/", Queue]). + +-spec decode_pid(kz_term:ne_binary()) -> kz_term:api_pid(). +decode_pid(<<"pid://", Pid/binary>>) -> + case binary:split(Pid, <<"/">>) of + [Pid, _RK] -> kz_term:to_pid(Pid); + _ -> 'undefined' + end; +decode_pid(_Queue) -> 'undefined'. + diff --git a/core/kazoo_amqp/src/api/kapi_sms.erl b/core/kazoo_amqp/src/api/kapi_sms.erl index 27fe8dc52fe..611bea436dd 100644 --- a/core/kazoo_amqp/src/api/kapi_sms.erl +++ b/core/kazoo_amqp/src/api/kapi_sms.erl @@ -14,12 +14,12 @@ ,outbound/1, outbound_v/1 ,bind_q/2, unbind_q/2 ,declare_exchanges/0 - ,publish_message/1, publish_message/2 - ,publish_delivery/1, publish_delivery/2 - ,publish_targeted_delivery/2, publish_targeted_delivery/3 - ,publish_resume/1, publish_resume/2 + ,publish_message/1 + ,publish_delivery/1 + ,publish_targeted_delivery/2 + ,publish_resume/1 ,publish_inbound/1, publish_inbound/2 - ,publish_outbound/1, publish_outbound/3 + ,publish_outbound/1, publish_outbound/2 ]). -include_lib("kz_amqp_util.hrl"). @@ -38,8 +38,8 @@ ,<<"Outbound-Callee-ID-Name">>, <<"Outbound-Callee-ID-Number">> ,<<"Caller-ID-Name">>, <<"Caller-ID-Number">> ,<<"Callee-ID-Name">>, <<"Callee-ID-Number">> - ,<<"From-User">>, <<"From-Realm">>, <<"From-URI">> - ,<<"To-User">>, <<"To-Realm">>, <<"To-URI">> + ,<<"From">>, <<"From-User">>, <<"From-Realm">>, <<"From-URI">> + ,<<"To">>, <<"To-User">>, <<"To-Realm">>, <<"To-URI">> ,<<"Dial-Endpoint-Method">> ,<<"Custom-Channel-Vars">>, <<"Custom-SIP-Headers">> ,<<"SIP-Transport">>, <<"SIP-Headers">> @@ -67,6 +67,10 @@ ,kz_amqp_util:encode(CallId) ]) ). +-define(BIND_SMS_ROUTING_KEY(Props) + ,?SMS_ROUTING_KEY(bind_route_id(Props), bind_call_id(Props))). +-define(PUBLISH_SMS_ROUTING_KEY(Props) + ,?SMS_ROUTING_KEY(route_id(Props), call_id(Props))). %% SMS Endpoints -define(SMS_REQ_ENDPOINT_HEADERS, [<<"Invite-Format">>]). @@ -108,6 +112,10 @@ ,{<<"Event-Name">>, ?DELIVERY_REQ_EVENT_NAME} ]). -define(DELIVERY_ROUTING_KEY(CallId), <<"message.delivery.", (kz_amqp_util:encode(CallId))/binary>>). +-define(BIND_DELIVERY_ROUTING_KEY(Props) + ,?DELIVERY_ROUTING_KEY(bind_call_id(Props))). +-define(PUBLISH_DELIVERY_ROUTING_KEY(Props) + ,?DELIVERY_ROUTING_KEY(call_id(Props))). %% SMS Resume -define(RESUME_REQ_EVENT_NAME, <<"resume">>). @@ -118,23 +126,29 @@ ]). -define(RESUME_REQ_TYPES, []). -define(RESUME_ROUTING_KEY(CallId), <<"message.resume.", (kz_amqp_util:encode(CallId))/binary>>). +-define(BIND_RESUME_ROUTING_KEY(Props) + ,?RESUME_ROUTING_KEY(bind_call_id(Props))). +-define(PUBLISH_RESUME_ROUTING_KEY(Props) + ,?RESUME_ROUTING_KEY(call_id(Props))). %% Inbound -define(INBOUND_REQ_EVENT_NAME, <<"inbound">>). --define(INBOUND_HEADERS, [<<"Message-ID">>, <<"Body">>, <<"Route-ID">> - ,<<"Caller-ID-Number">>, <<"Callee-ID-Number">> +-define(INBOUND_HEADERS, [<<"Message-ID">>, <<"Body">> + ,<<"To">>, <<"From">> ]). -define(OPTIONAL_INBOUND_HEADERS, [<<"Geo-Location">>, <<"Orig-IP">>, <<"Orig-Port">> ,<<"Custom-Channel-Vars">>, <<"Custom-SIP-Headers">> ,<<"From-Network-Addr">> ,<<"Switch-Hostname">>, <<"Switch-Nodename">> + ,<<"Caller-ID-Number">>, <<"Callee-ID-Number">> ,<<"Caller-ID-Name">>, <<"Callee-ID-Name">> ,<<"Contact">>, <<"User-Agent">> ,<<"Contact-IP">>, <<"Contact-Port">>, <<"Contact-Username">> ,<<"To">>, <<"From">>, <<"Request">> ,<<"Account-ID">> ,<<"Delivery-Result-Code">>, <<"Delivery-Failure">>, <<"Status">> - ,<<"Route-Type">>, <<"System-ID">> + ,<<"Route-Type">>, <<"System-ID">>, <<"Route-ID">> + ,<<"Custom-Channel-Vars">>, <<"Custom-SIP-Headers">> ]). -define(INBOUND_TYPES, [{<<"To">>, fun is_binary/1} ,{<<"From">>, fun is_binary/1} @@ -152,29 +166,38 @@ ]). -define(INBOUND_REQ_VALUES, [{<<"Event-Category">>, ?EVENT_CATEGORY} ,{<<"Event-Name">>, ?INBOUND_REQ_EVENT_NAME} - ,{<<"Route-Type">>, [<<"on-net">>, <<"off-net">>]} + ,{<<"Route-Type">>, [<<"onnet">>, <<"offnet">>, <<"api">>]} ]). --define(INBOUND_ROUTING_KEY(RouteId, CallId), <<"message.inbound." - ,(kz_amqp_util:encode(?LOWER(RouteId)))/binary, "." - ,(kz_amqp_util:encode(CallId))/binary - >>). +-define(INBOUND_ROUTING_KEY(RouteType, CallId), <<"message.inbound." + ,(kz_amqp_util:encode(?LOWER(RouteType)))/binary, "." + ,(kz_amqp_util:encode(CallId))/binary + >>). +-define(BIND_INBOUND_ROUTING_KEY(Props) + ,?INBOUND_ROUTING_KEY(bind_route_type(Props), bind_call_id(Props))). +-define(PUBLISH_INBOUND_ROUTING_KEY(Props) + ,?INBOUND_ROUTING_KEY(route_type(Props), call_id(Props))). %% Outbound -define(OUTBOUND_REQ_EVENT_NAME, <<"outbound">>). --define(OUTBOUND_HEADERS, [<<"Message-ID">>, <<"Body">>, <<"Route-ID">> - ,<<"Caller-ID-Number">>, <<"Callee-ID-Number">> +-define(OUTBOUND_HEADERS, [<<"Message-ID">> + ,<<"Body">> + ,<<"From">> + ,<<"To">> ]). -define(OPTIONAL_OUTBOUND_HEADERS, [<<"Geo-Location">>, <<"Orig-IP">>, <<"Orig-Port">> ,<<"Custom-Channel-Vars">>, <<"Custom-SIP-Headers">> ,<<"From-Network-Addr">> ,<<"Switch-Hostname">>, <<"Switch-Nodename">> + ,<<"Caller-ID-Number">>, <<"Callee-ID-Number">> ,<<"Caller-ID-Name">>, <<"Callee-ID-Name">> ,<<"Contact">>, <<"User-Agent">> ,<<"Contact-IP">>, <<"Contact-Port">>, <<"Contact-Username">> ,<<"To">>, <<"From">>, <<"Request">> - ,<<"Account-ID">> + ,<<"Account-ID">>, <<"Application-ID">> ,<<"Delivery-Result-Code">>, <<"Delivery-Failure">>, <<"Status">> - ,<<"Route-Type">>, <<"System-ID">> + ,<<"Route-Type">>, <<"System-ID">>, <<"Route-ID">> + ,<<"Originator-Properties">>, <<"Target-Properties">> + ,<<"Originator-Flags">>, <<"Target-Flags">> ]). -define(OUTBOUND_TYPES, [{<<"To">>, fun is_binary/1} ,{<<"From">>, fun is_binary/1} @@ -194,12 +217,11 @@ ,{<<"Event-Name">>, ?OUTBOUND_REQ_EVENT_NAME} ,{<<"Route-Type">>, [<<"on-net">>, <<"off-net">>]} ]). --define(OUTBOUND_ROUTING_KEY(RouteId, CallId) - ,list_to_binary(["message.outbound." - ,kz_amqp_util:encode(?LOWER(RouteId)), "." - ,kz_amqp_util:encode(CallId) - ]) - ). +-define(OUTBOUND_ROUTING_KEY(RouteId, CallId), outbound_routing_key(RouteId, CallId)). +-define(BIND_OUTBOUND_ROUTING_KEY(Props) + ,?OUTBOUND_ROUTING_KEY(bind_route_id(Props), bind_call_id(Props))). +-define(PUBLISH_OUTBOUND_ROUTING_KEY(Props) + ,?OUTBOUND_ROUTING_KEY(route_id(Props), call_id(Props))). -spec message(kz_term:api_terms()) -> api_formatter_return(). message(Prop) when is_list(Prop) -> @@ -299,65 +321,103 @@ resume_v(JObj) -> resume_v(kz_json:to_proplist(JObj)). %%------------------------------------------------------------------------------ -spec bind_q(kz_term:ne_binary(), kz_term:proplist()) -> 'ok'. bind_q(Queue, Props) -> - CallId = props:get_value('call_id', Props, props:get_value('message_id', Props, <<"*">>)), - RouteId = ?LOWER(props:get_value('route_id', Props, <<"*">>)), - Exchange = props:get_value('exchange', Props, ?SMS_EXCHANGE), - bind_q(Exchange, Queue, CallId, RouteId, props:get_value('restrict_to', Props)). - --spec bind_q(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:proplist()) -> 'ok'. -bind_q(Exchange, Queue, CallId, RouteId, 'undefined') -> - kz_amqp_util:bind_q_to_exchange(Queue, ?SMS_ROUTING_KEY(RouteId,CallId), Exchange), - kz_amqp_util:bind_q_to_exchange(Queue, ?DELIVERY_ROUTING_KEY(CallId), Exchange), - kz_amqp_util:bind_q_to_exchange(Queue, ?RESUME_ROUTING_KEY(CallId), Exchange), - kz_amqp_util:bind_q_to_exchange(Queue, ?INBOUND_ROUTING_KEY(RouteId, CallId), Exchange), - kz_amqp_util:bind_q_to_exchange(Queue, ?OUTBOUND_ROUTING_KEY(RouteId, CallId), Exchange); -bind_q(Exchange, Queue, CallId, RouteId, ['route'|Restrict]) -> - kz_amqp_util:bind_q_to_exchange(Queue, ?SMS_ROUTING_KEY(RouteId,CallId), Exchange), - bind_q(Exchange, Queue, CallId, RouteId, Restrict); -bind_q(Exchange, Queue, CallId, RouteId, ['delivery'|Restrict]) -> - kz_amqp_util:bind_q_to_exchange(Queue, ?DELIVERY_ROUTING_KEY(CallId), Exchange), - bind_q(Exchange, Queue, CallId, RouteId, Restrict); -bind_q(Exchange, Queue, CallId, RouteId, ['resume'|Restrict]) -> - kz_amqp_util:bind_q_to_exchange(Queue, ?RESUME_ROUTING_KEY(CallId), Exchange), - bind_q(Exchange, Queue, CallId, RouteId, Restrict); -bind_q(Exchange, Queue, CallId, RouteId, ['inbound'|Restrict]) -> - kz_amqp_util:bind_q_to_exchange(Queue, ?INBOUND_ROUTING_KEY(RouteId, CallId), Exchange), - bind_q(Exchange, Queue, CallId, RouteId, Restrict); -bind_q(Exchange, Queue, CallId, RouteId, ['outbound'|Restrict]) -> - kz_amqp_util:bind_q_to_exchange(Queue, ?OUTBOUND_ROUTING_KEY(RouteId, CallId), Exchange), - bind_q(Exchange, Queue, CallId, RouteId, Restrict); -bind_q(_, _, _, _, []) -> 'ok'. + bind_q(Queue, Props, props:get_value('restrict_to', Props)). + +-spec bind_q(kz_term:ne_binary(), kz_term:proplist(), kz_term:proplist()) -> 'ok'. +bind_q(Queue, Props, 'undefined') -> + kz_amqp_util:bind_q_to_exchange(Queue, ?BIND_SMS_ROUTING_KEY(Props), bind_exchange_id(Props)), + kz_amqp_util:bind_q_to_exchange(Queue, ?BIND_DELIVERY_ROUTING_KEY(Props), bind_exchange_id(Props)), + kz_amqp_util:bind_q_to_exchange(Queue, ?BIND_RESUME_ROUTING_KEY(Props), bind_exchange_id(Props)), + kz_amqp_util:bind_q_to_exchange(Queue, ?BIND_INBOUND_ROUTING_KEY(Props), bind_exchange_id(Props)), + kz_amqp_util:bind_q_to_exchange(Queue, ?BIND_OUTBOUND_ROUTING_KEY(Props), bind_exchange_id(Props)); +bind_q(Queue, Props, ['route'|Restrict]) -> + kz_amqp_util:bind_q_to_exchange(Queue, ?BIND_SMS_ROUTING_KEY(Props), bind_exchange_id(Props)), + bind_q(Queue, Props, Restrict); +bind_q(Queue, Props, ['delivery'|Restrict]) -> + kz_amqp_util:bind_q_to_exchange(Queue, ?BIND_DELIVERY_ROUTING_KEY(Props), bind_exchange_id(Props)), + bind_q(Queue, Props, Restrict); +bind_q(Queue, Props, ['resume'|Restrict]) -> + kz_amqp_util:bind_q_to_exchange(Queue, ?BIND_RESUME_ROUTING_KEY(Props), bind_exchange_id(Props)), + bind_q(Queue, Props, Restrict); +bind_q(Queue, Props, ['inbound'|Restrict]) -> + kz_amqp_util:bind_q_to_exchange(Queue, ?BIND_INBOUND_ROUTING_KEY(Props), bind_exchange_id(Props)), + bind_q(Queue, Props, Restrict); +bind_q(Queue, Props, ['outbound'|Restrict]) -> + kz_amqp_util:bind_q_to_exchange(Queue, ?BIND_OUTBOUND_ROUTING_KEY(Props), bind_exchange_id(Props)), + bind_q(Queue, Props, Restrict); +bind_q( _, _, []) -> 'ok'. -spec unbind_q(kz_term:ne_binary(), kz_term:proplist()) -> 'ok'. unbind_q(Queue, Props) -> - CallId = props:get_value('call_id', Props, props:get_value('message_id', Props, <<"*">>)), - RouteId = ?LOWER(props:get_value('route_id', Props, <<"*">>)), - Exchange = props:get_value('exchange', Props, ?SMS_EXCHANGE), - unbind_q(Exchange, Queue, CallId, RouteId, props:get_value('restrict_to', Props)). - --spec unbind_q(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:proplist()) -> 'ok'. -unbind_q(Exchange, Queue, CallId, RouteId, 'undefined') -> - 'ok' = kz_amqp_util:unbind_q_from_exchange(Queue, ?SMS_ROUTING_KEY(RouteId,CallId), Exchange), - 'ok' = kz_amqp_util:unbind_q_from_exchange(Queue, ?DELIVERY_ROUTING_KEY(CallId), Exchange), - 'ok' = kz_amqp_util:unbind_q_from_exchange(Queue, ?RESUME_ROUTING_KEY(CallId), Exchange), - 'ok' = kz_amqp_util:unbind_q_from_exchange(Queue, ?INBOUND_ROUTING_KEY(RouteId, CallId), Exchange), - kz_amqp_util:unbind_q_from_exchange(Queue, ?OUTBOUND_ROUTING_KEY(RouteId, CallId), Exchange); -unbind_q(Exchange, Queue, CallId, RouteId, ['route'|Restrict]) -> - 'ok' = kz_amqp_util:unbind_q_from_exchange(Queue, ?SMS_ROUTING_KEY(RouteId,CallId), Exchange), - unbind_q(Exchange, Queue, CallId, RouteId, Restrict); -unbind_q(Exchange, Queue, CallId, RouteId, ['delivery'|Restrict]) -> - 'ok' = kz_amqp_util:unbind_q_from_exchange(Queue, ?DELIVERY_ROUTING_KEY(CallId), Exchange), - unbind_q(Exchange, Queue, CallId, RouteId, Restrict); -unbind_q(Exchange, Queue, CallId, RouteId, ['resume'|Restrict]) -> - 'ok' = kz_amqp_util:unbind_q_from_exchange(Queue, ?RESUME_ROUTING_KEY(CallId), Exchange), - unbind_q(Exchange, Queue, CallId, RouteId, Restrict); -unbind_q(Exchange, Queue, CallId, RouteId, ['inbound'|Restrict]) -> - 'ok' = kz_amqp_util:unbind_q_from_exchange(Queue, ?INBOUND_ROUTING_KEY(RouteId, CallId), Exchange), - unbind_q(Exchange, Queue, CallId, RouteId, Restrict); -unbind_q(Exchange, Queue, CallId, RouteId, ['outbound'|Restrict]) -> - 'ok' = kz_amqp_util:unbind_q_from_exchange(Queue, ?OUTBOUND_ROUTING_KEY(RouteId, CallId), Exchange), - unbind_q(Exchange, Queue, CallId, RouteId, Restrict); -unbind_q(_, _, _, _, []) -> 'ok'. + unbind_q(Queue, Props, props:get_value('restrict_to', Props)). + +-spec unbind_q(kz_term:ne_binary(), kz_term:proplist(), kz_term:proplist()) -> 'ok'. +unbind_q(Queue, Props, 'undefined') -> + _ = kz_amqp_util:unbind_q_from_exchange(Queue, ?BIND_SMS_ROUTING_KEY(Props), bind_exchange_id(Props)), + _ = kz_amqp_util:unbind_q_from_exchange(Queue, ?BIND_DELIVERY_ROUTING_KEY(Props), bind_exchange_id(Props)), + _ = kz_amqp_util:unbind_q_from_exchange(Queue, ?BIND_RESUME_ROUTING_KEY(Props), bind_exchange_id(Props)), + _ = kz_amqp_util:unbind_q_from_exchange(Queue, ?BIND_INBOUND_ROUTING_KEY(Props), bind_exchange_id(Props)), + kz_amqp_util:unbind_q_from_exchange(Queue, ?BIND_OUTBOUND_ROUTING_KEY(Props), bind_exchange_id(Props)); +unbind_q(Queue, Props, ['route'|Restrict]) -> + _ = kz_amqp_util:unbind_q_from_exchange(Queue, ?BIND_SMS_ROUTING_KEY(Props), bind_exchange_id(Props)), + unbind_q(Queue, Props, Restrict); +unbind_q(Queue, Props, ['delivery'|Restrict]) -> + _ = kz_amqp_util:unbind_q_from_exchange(Queue, ?BIND_DELIVERY_ROUTING_KEY(Props), bind_exchange_id(Props)), + unbind_q(Queue, Props, Restrict); +unbind_q(Queue, Props, ['resume'|Restrict]) -> + _ = kz_amqp_util:unbind_q_from_exchange(Queue, ?BIND_RESUME_ROUTING_KEY(Props), bind_exchange_id(Props)), + unbind_q(Queue, Props, Restrict); +unbind_q(Queue, Props, ['inbound'|Restrict]) -> + _ = kz_amqp_util:unbind_q_from_exchange(Queue, ?BIND_INBOUND_ROUTING_KEY(Props), bind_exchange_id(Props)), + unbind_q(Queue, Props, Restrict); +unbind_q(Queue, Props, ['outbound'|Restrict]) -> + _ = kz_amqp_util:unbind_q_from_exchange(Queue, ?BIND_OUTBOUND_ROUTING_KEY(Props), bind_exchange_id(Props)), + unbind_q(Queue, Props, Restrict); +unbind_q( _, _, []) -> 'ok'. + +-spec bind_exchange_id(kz_term:api_terms()) -> kz_term:ne_binary(). +bind_exchange_id(Props) -> + props:get_value('exchange', Props, ?SMS_EXCHANGE). + +-spec bind_call_id(kz_term:api_terms()) -> kz_term:ne_binary(). +bind_call_id(Props) -> + props:get_value('call_id', Props, props:get_value('message_id', Props, <<"*">>)). + +-spec bind_route_id(kz_term:api_terms()) -> kz_term:api_ne_binary(). +bind_route_id(Props) -> + props:get_value('route_id', Props). + +-spec bind_route_type(kz_term:api_terms()) -> kz_term:ne_binary(). +bind_route_type(Props) -> + ?LOWER(props:get_value('route_type', Props, <<"*">>)). + +-spec exchange_id(kz_term:api_terms()) -> kz_term:ne_binary(). +exchange_id(Props) + when is_list(Props) -> + props:get_value(<<"Exchange-ID">>, Props, ?SMS_EXCHANGE); +exchange_id(JObj) -> + exchange_id(kz_json:to_proplist(JObj)). + +-spec call_id(kz_term:api_terms()) -> kz_term:ne_binary(). +call_id(Props) + when is_list(Props) -> + props:get_value(<<"Call-ID">>, Props, props:get_value(<<"Message-ID">>, Props)); +call_id(JObj) -> + call_id(kz_json:to_proplist(JObj)). + +-spec route_id(kz_term:api_terms()) -> kz_term:api_ne_binary(). +route_id(Props) + when is_list(Props) -> + ?LOWER(props:get_value(<<"Route-ID">>, Props)); +route_id(JObj) -> + route_id(kz_json:to_proplist(JObj)). + +-spec route_type(kz_term:api_terms()) -> kz_term:ne_binary(). +route_type(Props) + when is_list(Props) -> + ?LOWER(props:get_value(<<"Route-Type">>, Props)); +route_type(JObj) -> + route_type(kz_json:to_proplist(JObj)). %%------------------------------------------------------------------------------ %% @doc Declare the exchanges used by this API. @@ -368,74 +428,67 @@ declare_exchanges() -> kz_amqp_util:new_exchange(?SMS_EXCHANGE, <<"topic">>). -spec publish_message(kz_term:api_terms()) -> 'ok'. -publish_message(JObj) -> - publish_message(JObj, ?DEFAULT_CONTENT_TYPE). - --spec publish_message(kz_term:api_terms(), binary()) -> 'ok'. -publish_message(Req, ContentType) -> +publish_message(Req) -> {'ok', Payload} = kz_api:prepare_api_payload(Req, ?SMS_REQ_VALUES, fun message/1), CallId = props:get_value(<<"Call-ID">>, Req), RouteId = props:get_value(<<"Route-ID">>, Req, <<"*">>), Exchange = props:get_value(<<"Exchange-ID">>, Req, ?SMS_EXCHANGE), - kz_amqp_util:basic_publish(Exchange, ?SMS_ROUTING_KEY(RouteId, CallId), Payload, ContentType). + kz_amqp_util:basic_publish(Exchange, ?SMS_ROUTING_KEY(RouteId, CallId), Payload). -spec publish_inbound(kz_term:api_terms()) -> 'ok'. -publish_inbound(JObj) -> - publish_inbound(JObj, ?DEFAULT_CONTENT_TYPE). +publish_inbound(Req) -> + publish_inbound(Req, []). --spec publish_inbound(kz_term:api_terms(), binary()) -> 'ok'. -publish_inbound(Req, ContentType) -> +-spec publish_inbound(kz_term:api_terms(), kz_term:proplist()) -> 'ok'. +publish_inbound(Req, AMQPOptions) -> {'ok', Payload} = kz_api:prepare_api_payload(Req, ?INBOUND_REQ_VALUES, fun inbound/1), - MessageId = props:get_value(<<"Message-ID">>, Req), - RouteId = props:get_value(<<"Route-ID">>, Req, <<"*">>), - Exchange = props:get_value(<<"Exchange-ID">>, Req, ?SMS_EXCHANGE), - kz_amqp_util:basic_publish(Exchange, ?INBOUND_ROUTING_KEY(RouteId, MessageId), Payload, ContentType). + Exchange = exchange_id(Req), + kz_amqp_util:basic_publish(Exchange, ?PUBLISH_INBOUND_ROUTING_KEY(Req), Payload, ?DEFAULT_CONTENT_TYPE, AMQPOptions). -spec publish_outbound(kz_term:api_terms()) -> 'ok'. publish_outbound(JObj) -> - publish_outbound(JObj, ?DEFAULT_CONTENT_TYPE, []). + publish_outbound(JObj, []). --spec publish_outbound(kz_term:api_terms(), binary(), list()) -> 'ok'. -publish_outbound(Req, ContentType, AMQPOptions) -> +-spec publish_outbound(kz_term:api_terms(), kz_term:proplist()) -> 'ok'. +publish_outbound(Req, AMQPOptions) -> {'ok', Payload} = kz_api:prepare_api_payload(Req, ?OUTBOUND_REQ_VALUES, fun outbound/1), - MessageId = props:get_value(<<"Message-ID">>, Req), - RouteId = props:get_value(<<"Route-ID">>, Req, <<"*">>), - Exchange = props:get_value(<<"Exchange-ID">>, Req, ?SMS_EXCHANGE), - RK = ?OUTBOUND_ROUTING_KEY(RouteId, MessageId), - kz_amqp_util:basic_publish(Exchange, RK, Payload, ContentType, AMQPOptions). + Exchange = exchange_id(Req), + RK = ?PUBLISH_OUTBOUND_ROUTING_KEY(Req), + kz_amqp_util:basic_publish(Exchange, RK, Payload, ?DEFAULT_CONTENT_TYPE, AMQPOptions). -spec publish_delivery(kz_term:api_terms()) -> 'ok'. -publish_delivery(JObj) -> - publish_delivery(JObj, ?DEFAULT_CONTENT_TYPE). - --spec publish_delivery(kz_term:api_terms(), binary()) -> 'ok'. -publish_delivery(Req, ContentType) -> +publish_delivery(Req) -> {'ok', Payload} = kz_api:prepare_api_payload(Req, ?DELIVERY_REQ_VALUES, fun delivery/1), CallId = props:get_value(<<"Call-ID">>, Req), Exchange = props:get_value(<<"Exchange-ID">>, Req, ?SMS_EXCHANGE), - kz_amqp_util:basic_publish(Exchange, ?DELIVERY_ROUTING_KEY(CallId), Payload, ContentType). + kz_amqp_util:basic_publish(Exchange, ?DELIVERY_ROUTING_KEY(CallId), Payload). -spec publish_targeted_delivery(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. publish_targeted_delivery(RespQ, JObj) -> - publish_targeted_delivery(RespQ, JObj, ?DEFAULT_CONTENT_TYPE). - --spec publish_targeted_delivery(kz_term:ne_binary(), kz_term:api_terms(), binary()) -> 'ok'. -publish_targeted_delivery(RespQ, JObj, ContentType) -> {'ok', Payload} = kz_api:prepare_api_payload(JObj, ?DELIVERY_REQ_VALUES, fun delivery/1), - kz_amqp_util:targeted_publish(RespQ, Payload, ContentType). + kz_amqp_util:targeted_publish(RespQ, Payload). -spec publish_resume(kz_term:api_terms() | kz_term:ne_binary()) -> 'ok'. publish_resume(SMS) when is_binary(SMS) -> Payload = [{<<"SMS-ID">>, SMS} | kz_api:default_headers(<<"API">>, <<"0.9.7">>) ], - publish_resume(Payload, ?DEFAULT_CONTENT_TYPE); -publish_resume(JObj) -> - publish_resume(JObj, ?DEFAULT_CONTENT_TYPE). - --spec publish_resume(kz_term:api_terms(), binary()) -> 'ok'. -publish_resume(Req, ContentType) -> + publish_resume(Payload); +publish_resume(Req) -> {'ok', Payload} = kz_api:prepare_api_payload(Req, ?RESUME_REQ_VALUES, fun resume/1), CallId = props:get_value(<<"Call-ID">>, Req), Exchange = props:get_value(<<"Exchange-ID">>, Req, ?SMS_EXCHANGE), - kz_amqp_util:basic_publish(Exchange, ?RESUME_ROUTING_KEY(CallId), Payload, ContentType). + kz_amqp_util:basic_publish(Exchange, ?RESUME_ROUTING_KEY(CallId), Payload). + +-spec outbound_routing_key(kz_term:api_ne_binary(), kz_term:ne_binary()) -> kz_term:ne_binary(). +outbound_routing_key(RouteId, CallId) -> + Parts = ["message" + ,"outbound" + ,to_lower(RouteId) + ,CallId + ], + kz_binary:join(lists:filter(fun(Part) -> Part =/= 'undefined' end, Parts), <<".">>). + +-spec to_lower(term()) -> kz_term:api_ne_binary(). +to_lower('undefined') -> 'undefined'; +to_lower(Term) -> kz_term:to_lower_binary(Term). diff --git a/core/kazoo_amqp/src/gen_listener.erl b/core/kazoo_amqp/src/gen_listener.erl index caf6a7ffa3a..213e5dae0c0 100644 --- a/core/kazoo_amqp/src/gen_listener.erl +++ b/core/kazoo_amqp/src/gen_listener.erl @@ -875,14 +875,31 @@ format_status(_Opt end. -spec distribute_event(kz_json:object(), deliver(), state()) -> state(). -distribute_event(JObj, Deliver, State) -> - case callback_handle_event(JObj, Deliver, State) of +distribute_event(JObj, {_ , #'P_basic'{headers='undefined'}}=BasicDeliver, State) -> + case callback_handle_event(JObj, BasicDeliver, State) of 'ignore' -> State; {'ignore', ModuleState} -> State#state{module_state=ModuleState}; - {CallbackData, ModuleState} -> distribute_event(CallbackData, JObj, Deliver, State#state{module_state=ModuleState}); - CallbackData -> distribute_event(CallbackData, JObj, Deliver, State) + {CallbackData, ModuleState} -> distribute_event(CallbackData, JObj, BasicDeliver, State#state{module_state=ModuleState}); + CallbackData -> distribute_event(CallbackData, JObj, BasicDeliver, State) + end; +distribute_event(JObj, {Deliver, #'P_basic'{headers=Headers}=Basic}=BasicDeliver, State) -> + case lists:keyfind(?KEY_DELIVER_TO_PID, 1, Headers) of + {?KEY_DELIVER_TO_PID, _, Pid} -> + Props = [{'basic', Basic} + ,{'deliver', Deliver} + ], + kz_term:to_pid(Pid) ! {'kapi', kapi:delivery_message(JObj, Props)}, + {'noreply', State}; + 'false' -> + case callback_handle_event(JObj, BasicDeliver, State) of + 'ignore' -> State; + {'ignore', ModuleState} -> State#state{module_state=ModuleState}; + {CallbackData, ModuleState} -> distribute_event(CallbackData, JObj, BasicDeliver, State#state{module_state=ModuleState}); + CallbackData -> distribute_event(CallbackData, JObj, BasicDeliver, State) + end end. + -spec distribute_event(callback_data(), kz_json:object(), deliver(), state()) -> state(). distribute_event(CallbackData ,JObj diff --git a/core/kazoo_amqp/src/kz_amqp_util.erl b/core/kazoo_amqp/src/kz_amqp_util.erl index 7e8adf99565..da1b4f5869d 100644 --- a/core/kazoo_amqp/src/kz_amqp_util.erl +++ b/core/kazoo_amqp/src/kz_amqp_util.erl @@ -505,6 +505,15 @@ basic_publish(Exchange, RoutingKey, Payload, ContentType) -> basic_publish(Exchange, RoutingKey, Payload, ContentType, Prop) when is_list(Payload) -> basic_publish(Exchange, RoutingKey, iolist_to_binary(Payload), ContentType, Prop); +basic_publish(Exchange, <<"pid://", _/binary>>=RoutingKey, ?NE_BINARY = Payload, ContentType, Props) + when is_binary(Exchange), + is_binary(RoutingKey), + is_binary(ContentType), + is_list(Props) -> + Headers = props:get_value('headers', Props, []), + {'match', [Pid, RK]}= re:run(RoutingKey, <<"pid://(.*)/(.*)">>, [{'capture', [1,2], 'binary'}]), + NewProps = props:set_value('headers', [{?KEY_DELIVER_TO_PID, binary, Pid} | Headers], Props), + basic_publish(Exchange, RK, Payload, ContentType, NewProps); basic_publish(Exchange, RoutingKey, ?NE_BINARY = Payload, ContentType, Props) when is_binary(Exchange), is_binary(RoutingKey), diff --git a/core/kazoo_amqp/src/listener_types.hrl b/core/kazoo_amqp/src/listener_types.hrl index a9018ad11df..1112dba53a7 100644 --- a/core/kazoo_amqp/src/listener_types.hrl +++ b/core/kazoo_amqp/src/listener_types.hrl @@ -27,7 +27,9 @@ {'basic_qos', non_neg_integer()} | {'broker' | 'broker_tag', kz_term:ne_binary()} | {'declare_exchanges', declare_exchanges()} | - {'auto_ack', boolean()} + {'auto_ack', boolean()} | + {'server_confirms', boolean()} | + {'channel_flow', boolean()} ]. -type responder_callback_fun2() :: fun((kz_json:object(), kz_term:proplist()) -> any()). diff --git a/core/kazoo_call/src/kapps_call.erl b/core/kazoo_call/src/kapps_call.erl index f271f3086d6..2736cdbf27b 100644 --- a/core/kazoo_call/src/kapps_call.erl +++ b/core/kazoo_call/src/kapps_call.erl @@ -71,7 +71,7 @@ -export([get_prompt/2, get_prompt/3]). -export([set_to_tag/2, to_tag/1]). -export([set_from_tag/2, from_tag/1]). --export([direction/1]). +-export([set_direction/2, direction/1]). -export([set_call_bridged/2, call_bridged/1]). -export([set_message_left/2, message_left/1]). @@ -223,6 +223,7 @@ ,{<<"Caller-ID-Number">>, #kapps_call.caller_id_number} ,{<<"Fetch-ID">>, #kapps_call.fetch_id} ,{<<"Owner-ID">>, #kapps_call.owner_id} + ,{<<"Inception">>, #kapps_call.inception} ]). -spec default_helper_function(Field, call()) -> Field. @@ -287,7 +288,7 @@ from_route_req(RouteReq, #kapps_call{call_id=OldCallId ,origination_call_id=kz_json:get_ne_binary_value(<<"Origination-Call-ID">>, RouteReq, origination_call_id(Call1)) ,context=kz_json:get_ne_binary_value(<<"Context">>, RouteReq, context(Call)) ,request=Request - ,request_user=to_e164(RequestUser) + ,request_user=to_e164(RequestUser, AccountId) ,request_realm=RequestRealm ,from=From ,from_user=FromUser @@ -873,19 +874,21 @@ callee_id_number(#kapps_call{callee_id_number='undefined'}) -> <<>>; callee_id_number(#kapps_call{callee_id_number=CIDNumber}) -> CIDNumber. -spec set_request(kz_term:ne_binary(), call()) -> call(). -set_request(Request, #kapps_call{}=Call) when is_binary(Request) -> +set_request(Request, #kapps_call{account_id=AccountId}=Call) when is_binary(Request) -> [RequestUser, RequestRealm] = binary:split(Request, <<"@">>), Call#kapps_call{request=Request - ,request_user=to_e164(RequestUser) + ,request_user=to_e164(RequestUser, AccountId) ,request_realm=RequestRealm }. -ifdef(TEST). -to_e164(Number) -> Number. +to_e164(Number, _AccountId) -> Number. -else. -to_e164(<<"*", _/binary>>=Number) -> Number; -to_e164(Number) -> - knm_converters:normalize(Number). +to_e164(<<"*", _/binary>>=Number, _AccountId) -> Number; +to_e164(Number, 'undefined') -> + knm_converters:normalize(Number); +to_e164(Number, AccountId) -> + knm_converters:normalize(Number, AccountId). -endif. -spec request(call()) -> kz_term:ne_binary(). @@ -1174,6 +1177,10 @@ from_tag(#kapps_call{from_tag=FromTag}) -> direction(#kapps_call{direction=Direction}) -> Direction. +-spec set_direction(kz_term:ne_binary(), call()) -> call(). +set_direction(Direction, #kapps_call{}=Call) when is_binary(Direction) -> + Call#kapps_call{direction=Direction}. + -spec call_bridged(call()) -> boolean(). call_bridged(#kapps_call{call_bridged=IsBridged}) -> kz_term:is_true(IsBridged). diff --git a/core/kazoo_call/src/kapps_sms_command.erl b/core/kazoo_call/src/kapps_sms_command.erl deleted file mode 100644 index c6010b63bba..00000000000 --- a/core/kazoo_call/src/kapps_sms_command.erl +++ /dev/null @@ -1,363 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @copyright (C) 2012-2019, 2600Hz -%%% @doc -%%% @author Karl Anderson -%%% @author James Aimonetti -%%% @end -%%%----------------------------------------------------------------------------- --module(kapps_sms_command). - --include("kapps_sms_command.hrl"). - --export([send_sms/2, send_sms/3 - ,send_amqp_sms/1, send_amqp_sms/2 - ]). --export([b_send_sms/2, b_send_sms/3, b_send_sms/4]). - --export([default_collect_timeout/0 - ,default_message_timeout/0 - ,default_application_timeout/0 - ]). - --define(CONFIG_CAT, <<"sms_command">>). - --define(DEFAULT_COLLECT_TIMEOUT - ,kapps_config:get_integer(?CONFIG_CAT, <<"collect_timeout">>, 60 * ?MILLISECONDS_IN_SECOND) - ). - --define(DEFAULT_MESSAGE_TIMEOUT, kapps_config:get_integer(?CONFIG_CAT, <<"message_timeout">>, 60 * ?MILLISECONDS_IN_SECOND)). - --define(DEFAULT_APPLICATION_TIMEOUT - ,kapps_config:get_integer(?CONFIG_CAT, <<"application_timeout">>, 500 * ?MILLISECONDS_IN_SECOND) - ). --define(DEFAULT_STRATEGY, <<"single">>). - --define(ATOM(X), kz_term:to_atom(X, 'true')). --define(SMS_POOL(A,B,C), ?ATOM(<>) ). - --define(SMS_DEFAULT_OUTBOUND_OPTIONS - ,kz_json:from_list([{<<"delivery_mode">>, 2} - ,{<<"mandatory">>, 'true'} - ]) - ). --define(SMS_OUTBOUND_OPTIONS_KEY, [<<"outbound">>, <<"options">>]). --define(SMS_OUTBOUND_OPTIONS - ,kapps_config:get_json(<<"sms">>, ?SMS_OUTBOUND_OPTIONS_KEY, ?SMS_DEFAULT_OUTBOUND_OPTIONS) - ). - --spec default_collect_timeout() -> pos_integer(). -default_collect_timeout() -> - ?DEFAULT_COLLECT_TIMEOUT. - --spec default_message_timeout() -> pos_integer(). -default_message_timeout() -> - ?DEFAULT_MESSAGE_TIMEOUT. - --spec default_application_timeout() -> pos_integer(). -default_application_timeout() -> - ?DEFAULT_APPLICATION_TIMEOUT. - --type kapps_api_sms_return() :: {'error', 'timeout' | kz_json:object()} | - {'ok', kz_json:object()}. - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec send_sms(kz_json:objects(), kapps_call:call()) -> 'ok'. -send_sms(Endpoints, Call) -> send_sms(Endpoints, ?DEFAULT_STRATEGY, Call). - --spec send_sms(kz_json:objects(), binary(), kapps_call:call()) -> 'ok'. -send_sms(EndpointList, Strategy, Call) -> - Endpoints = create_sms_endpoints(EndpointList, []), - API = create_sms(Call), - send(Strategy, API, Endpoints). - --spec b_send_sms(kz_json:objects(), kapps_call:call()) -> kapps_api_sms_return(). -b_send_sms(Endpoints, Call) -> b_send_sms(Endpoints, ?DEFAULT_STRATEGY, Call). - --spec b_send_sms(kz_json:objects(), binary(), kapps_call:call()) -> kapps_api_sms_return(). -b_send_sms(Endpoints, Strategy, Call) -> b_send_sms(Endpoints, Strategy, ?DEFAULT_MESSAGE_TIMEOUT, Call). - --spec b_send_sms(kz_json:objects(), binary(), integer(), kapps_call:call()) -> kapps_api_sms_return(). -b_send_sms(EndpointList, Strategy, Timeout, Call) -> - Endpoints = create_sms_endpoints(EndpointList, []), - API = create_sms(Call), - send_and_wait(Strategy, API, Endpoints, Timeout). - -send(<<"single">>, _API, []) -> - {'error', <<"no endpoints available">>}; -send(<<"single">>, API, [Endpoint | Others]) -> - CallId = props:get_value(<<"Call-ID">>, API), - Payload = props:set_values( - [{<<"Endpoints">>, [Endpoint]} - ,{<<"Callee-ID-Name">>, kz_json:get_value(<<"Callee-ID-Name">>, Endpoint)} - ,{<<"Callee-ID-Number">>, kz_json:get_value(<<"Callee-ID-Number">>, Endpoint)} - ,{<<"To-DID">>, kz_json:get_value(<<"To-DID">>, Endpoint)} - | kz_json:get_value(<<"Endpoint-Options">>, Endpoint, []) - ], API), - case kz_amqp_worker:cast(Payload, fun kapi_sms:publish_message/1) of - 'ok' -> 'ok'; - {'error', _R}=Err when Others =:= [] -> - lager:info("received error while sending msg ~s: ~-800p", [CallId, _R]), - Err; - {'error', _R} -> - lager:info("received error while sending msg ~s: ~-800p", [CallId, _R]), - lager:info("processing next endpoint."), - send(<<"single">>, API, Others) - end; -send(Strategy, _API, _Endpoints) -> - lager:debug("Strategy ~s not implemented", [Strategy]). - -send_and_wait(<<"single">>, _API, [], _Timeout) -> - {'error', <<"no endpoints available">>}; -send_and_wait(<<"single">>, API, [Endpoint| Others], Timeout) -> - CallId = props:get_value(<<"Call-ID">>, API), - Type = kz_json:get_value(<<"Endpoint-Type">>, Endpoint, <<"sip">>), - ReqResp = send(Type, API, Endpoint, Timeout), - case ReqResp of - {'error', _R}=Err when Others =:= [] -> - lager:info("received error while sending msg ~s: ~-800p", [CallId, _R]), - Err; - {'error', _R} -> - lager:info("received error while sending msg ~s: ~-800p", [CallId, _R]), - lager:info("processing next endpoint."), - send_and_wait(<<"single">>, API, Others, Timeout); - {_, _JObjs} = Ret -> - lager:debug("received sms delivery result for msg ~s", [CallId]), - Ret - end; -send_and_wait(Strategy, _API, _Endpoints, _Timeout) -> - lager:debug("Strategy ~s not implemented", [Strategy]). - -send(<<"sip">>, API, Endpoint, Timeout) -> - Options = kz_json:to_proplist(kz_json:get_value(<<"Endpoint-Options">>, Endpoint, [])), - Payload = props:set_values( [{<<"Endpoints">>, [Endpoint]} | Options], API), - CallId = props:get_value(<<"Call-ID">>, Payload), - lager:debug("sending sms and waiting for response ~s", [CallId]), - _ = kz_amqp_worker:cast(Payload, fun kapi_sms:publish_message/1), - wait_for_correlated_message(CallId, <<"delivery">>, <<"message">>, Timeout); -send(<<"amqp">>, API, Endpoint, _Timeout) -> - CallId = props:get_value(<<"Call-ID">>, API), - Options = kz_json:to_proplist(kz_json:get_value(<<"Endpoint-Options">>, Endpoint, [])), - Props = kz_json:to_proplist(Endpoint) ++ Options, - Broker = kz_json:get_value(<<"Route">>, Endpoint), - Exchange = kz_json:get_value([<<"Endpoint-Options">>, <<"Exchange-ID">>], Endpoint), - ExchangeType = kz_json:get_value([<<"Endpoint-Options">>, <<"Exchange-Type">>], Endpoint, <<"topic">>), - ExchangeOptions = amqp_exchange_options(kz_json:get_value([<<"Endpoint-Options">>, <<"Exchange-Options">>], Endpoint)), - RouteId = kz_json:get_value([<<"Endpoint-Options">>, <<"Route-ID">>], Endpoint), - BrokerName = kz_json:get_value([<<"Endpoint-Options">>, <<"Broker-Name">>], Endpoint, <<"noname">>), - FailOver = kz_json:get_value(<<"Failover">>, Endpoint), - maybe_add_broker(Broker, Exchange, RouteId, ExchangeType, ExchangeOptions, BrokerName), - - Payload = props:set_values(Props, API), - case send_amqp_sms(Payload, ?SMS_POOL(Exchange, RouteId, BrokerName)) of - 'ok' -> - DeliveryProps = props:filter_undefined( - [{<<"Delivery-Result-Code">>, <<"sip:200">> } - ,{<<"Status">>, <<"Success">>} - ,{<<"Message-ID">>, props:get_value(<<"Message-ID">>, API) } - ,{<<"Call-ID">>, CallId } - | kz_api:default_headers(<<"message">>, <<"delivery">>, ?APP_NAME, ?APP_VERSION) - ]), - {'ok', kz_json:set_values(DeliveryProps, kz_json:new())}; - {'error', {'timeout', _Reason}} when FailOver =:= 'undefined' -> - DeliveryProps = props:filter_undefined( - [{<<"Delivery-Result-Code">>, <<"sip:500">> } - ,{<<"Delivery-Failure">>, 'true'} - ,{<<"Error-Code">>, 500} - ,{<<"Error-Message">>, <<"timeout">>} - ,{<<"Status">>, <<"Failed">>} - ,{<<"Message-ID">>, props:get_value(<<"Message-ID">>, API) } - ,{<<"Call-ID">>, CallId } - | kz_api:default_headers(<<"message">>, <<"delivery">>, ?APP_NAME, ?APP_VERSION) - ]), - {'ok', kz_json:set_values(DeliveryProps, kz_json:new())}; - {'error', Reason} when FailOver =:= 'undefined' -> - DeliveryProps = props:filter_undefined( - [{<<"Delivery-Result-Code">>, <<"sip:500">> } - ,{<<"Delivery-Failure">>, 'true'} - ,{<<"Error-Code">>, 500} - ,{<<"Error-Message">>, kz_term:to_binary(Reason)} - ,{<<"Status">>, <<"Failed">>} - ,{<<"Message-ID">>, props:get_value(<<"Message-ID">>, API) } - ,{<<"Call-ID">>, CallId } - | kz_api:default_headers(<<"message">>, <<"delivery">>, ?APP_NAME, ?APP_VERSION) - ]), - {'ok', kz_json:set_values(DeliveryProps, kz_json:new())}; - {'error', Reason} -> - lager:info("received error while sending msg ~s: ~-800p", [CallId, Reason]), - lager:info("trying failover"), - send(<<"amqp">>, API, FailOver, _Timeout) - end. - --spec amqp_exchange_options(kz_term:api_object()) -> kz_term:proplist(). -amqp_exchange_options('undefined') -> []; -amqp_exchange_options(JObj) -> - [{kz_term:to_atom(K, 'true'), V} - || {K, V} <- kz_json:to_proplist(JObj) - ]. - --spec send_amqp_sms(kz_term:proplist()) -> 'ok' | {'error', any()}. -send_amqp_sms(Payload) -> - send_amqp_sms(Payload, kz_amqp_worker:worker_pool()). - --spec send_amqp_sms(kz_term:proplist(), atom()) -> 'ok' | {'error', any()}. -send_amqp_sms(Payload, Pool) -> - case kz_amqp_worker:cast(Payload, fun publish_outbound/1, Pool) of - 'ok' -> 'ok'; - {'error', _}=E -> E; - {'returned', _JObj, Deliver} -> - {'error', kz_json:get_value(<<"message">>, Deliver, <<"unknown">>)} - end. - -publish_outbound(Payload) -> - AMQPOptions = amqp_options(), - kapi_sms:publish_outbound(Payload, ?DEFAULT_CONTENT_TYPE, AMQPOptions). - -amqp_options() -> - amqp_options(?SMS_OUTBOUND_OPTIONS). - --spec amqp_options(kz_term:api_object()) -> kz_term:proplist(). -amqp_options('undefined') -> []; -amqp_options(JObj) -> - [{kz_term:to_atom(K, 'true'), V} - || {K, V} <- kz_json:to_proplist(JObj) - ]. - --spec maybe_add_broker(kz_term:api_binary(), kz_term:api_binary(), kz_term:api_binary(), kz_term:ne_binary(), kz_term:proplist(), kz_term:ne_binary()) -> 'ok'. -maybe_add_broker(Broker, Exchange, RouteId, ExchangeType, ExchangeOptions, BrokerName) -> - PoolPid = kz_amqp_sup:pool_pid(?SMS_POOL(Exchange, RouteId, BrokerName)), - maybe_add_broker(Broker, Exchange, RouteId, ExchangeType, ExchangeOptions, BrokerName, PoolPid). - --spec maybe_add_broker(kz_term:api_binary(), kz_term:api_binary(), kz_term:api_binary(), kz_term:ne_binary(), kz_term:proplist(), kz_term:ne_binary(), kz_term:api_pid()) -> 'ok'. -maybe_add_broker(Broker, Exchange, RouteId, ExchangeType, ExchangeOptions, BrokerName, 'undefined') -> - Exchanges = [{Exchange, ExchangeType, ExchangeOptions}], - _ = kz_amqp_sup:add_amqp_pool(?SMS_POOL(Exchange, RouteId, BrokerName), Broker, 5, 5, [], Exchanges, 'true'), - 'ok'; -maybe_add_broker(_Broker, _Exchange, _RouteId, _ExchangeType, _ExchangeOptions, _BrokerName, _Pid) -> 'ok'. - --spec create_sms(kapps_call:call()) -> kz_term:proplist(). -create_sms(Call) -> - AccountId = kapps_call:account_id(Call), - AccountRealm = kapps_call:to_realm(Call), - CCVUpdates = props:filter_undefined( - [{<<"Ignore-Display-Updates">>, <<"true">>} - ,{<<"Account-ID">>, AccountId} - ,{<<"Account-Realm">>, AccountRealm} - ,{<<"From-User">>, kapps_call:from_user(Call)} - ,{<<"From-Realm">>, kapps_call:from_realm(Call)} - ,{<<"From-URI">>, kapps_call:from(Call)} - ,{<<"Reseller-ID">>, kz_services_reseller:get_id(AccountId)} - ]), - [{<<"Message-ID">>, kapps_call:kvs_fetch(<<"Message-ID">>, Call)} - ,{<<"Call-ID">>, kapps_call:call_id(Call)} - ,{<<"Body">>, kapps_call:kvs_fetch(<<"Body">>, Call)} - ,{<<"From">>, kapps_call:from(Call)} - ,{<<"Caller-ID-Number">>, kapps_call:caller_id_number(Call)} - ,{<<"To">>, kapps_call:to(Call)} - ,{<<"Request">>, kapps_call:request(Call) } - ,{<<"Application-Name">>, <<"send">>} - ,{<<"Custom-Channel-Vars">>, kz_json:set_values(CCVUpdates, kz_json:new())} - | kz_api:default_headers(kapps_call:controller_queue(Call), ?APP_NAME, ?APP_VERSION) - ]. - --spec create_sms_endpoints(kz_json:objects(), kz_json:objects()) -> kz_json:objects(). -create_sms_endpoints([], Endpoints) -> Endpoints; -create_sms_endpoints([Endpoint | Others], Endpoints) -> - EndpointType = kz_json:get_value(<<"Endpoint-Type">>, Endpoint, <<"sip">>), - case create_sms_endpoint(Endpoint, EndpointType) of - 'undefined' -> create_sms_endpoints(Others, Endpoints); - NewEndpoint -> create_sms_endpoints(Others, [NewEndpoint | Endpoints]) - end. - --spec create_sms_endpoint(kz_json:object(), binary()) -> kz_term:api_object(). -create_sms_endpoint(Endpoint, <<"amqp">>) -> Endpoint; -create_sms_endpoint(Endpoint, <<"sip">>) -> - Realm = kz_json:get_value(<<"To-Realm">>, Endpoint), - Username = kz_json:get_value(<<"To-User">>, Endpoint), - case lookup_reg(Username, Realm) of - {'ok', Node} -> - Options = kz_json:get_value(<<"Endpoint-Options">>, Endpoint, []), - kz_json:set_values( - [{<<"Route-ID">>, Node} - ,{<<"Endpoint-Options">>, kz_json:from_list([{<<"Route-ID">>, Node} | Options])} - ], Endpoint); - {'error', _E} -> 'undefined' - end. - --spec lookup_reg(kz_term:ne_binary(), kz_term:ne_binary()) -> {'error', any()} | - {'ok', kz_term:ne_binary()}. -lookup_reg('undefined', _Realm) -> {'error', 'invalid_user'}; -lookup_reg(_Username, 'undefined') -> {'error', 'invalid_realm'}; -lookup_reg(Username, Realm) -> - Req = [{<<"Realm">>, Realm} - ,{<<"Username">>, Username} - ,{<<"Fields">>, [<<"Registrar-Node">>]} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ], - case kz_amqp_worker:call_collect(Req - ,fun kapi_registration:publish_query_req/1 - ,{'ecallmgr', 'true'} - ) - of - {'error', _E}=E -> - lager:debug("error getting registration: ~p", [_E]), - E; - {_, JObjs} -> - case extract_device_registrations(JObjs) of - [] -> {'error', 'not_registered'}; - [FirstNode | _Others] -> {'ok', FirstNode} - end - end. - --spec extract_device_registrations(kz_json:objects()) -> kz_term:ne_binaries(). -extract_device_registrations(JObjs) -> - sets:to_list(extract_device_registrations(JObjs, sets:new())). - --spec extract_device_registrations(kz_json:objects(), sets:set()) -> sets:set(). -extract_device_registrations([], Set) -> Set; -extract_device_registrations([JObj|JObjs], Set) -> - Fields = kz_json:get_value(<<"Fields">>, JObj, []), - S = lists:foldl(fun extract_device_registrar_fold/2, Set, Fields), - extract_device_registrations(JObjs, S). - --spec extract_device_registrar_fold(kz_json:object(), sets:set()) -> sets:set(). -extract_device_registrar_fold(JObj, Set) -> - case kz_json:get_ne_value(<<"Registrar-Node">>, JObj) of - 'undefined' -> Set; - AuthId -> sets:add_element(AuthId, Set) - end. - --spec get_correlated_msg_type(kz_json:object()) -> - {kz_term:api_binary(), kz_term:api_binary(), kz_term:api_binary()}. -get_correlated_msg_type(JObj) -> - get_correlated_msg_type(<<"Call-ID">>, JObj). - --spec get_correlated_msg_type(kz_term:ne_binary(), kz_json:object()) -> - {kz_term:api_binary(), kz_term:api_binary(), kz_term:api_binary()}. -get_correlated_msg_type(Key, JObj) -> - {C, N} = kz_util:get_event_type(JObj), - {C, N, kz_json:get_value(Key, JObj)}. - --spec wait_for_correlated_message(kz_term:ne_binary() | kapps_call:call(), kz_term:ne_binary(), kz_term:ne_binary(), timeout()) -> - kapps_api_std_return(). -wait_for_correlated_message(CallId, Event, Type, Timeout) when is_binary(CallId) -> - Start = os:timestamp(), - case kapps_call_command:receive_event(Timeout) of - {'error', 'timeout'}=E -> E; - {'ok', JObj}=Ok -> - case get_correlated_msg_type(JObj) of - {<<"error">>, _, CallId} -> - lager:debug("channel execution error while waiting for ~s", [CallId]), - {'error', JObj}; - {Type, Event, CallId } -> - Ok; - {_Type, _Event, _CallId} -> - lager:debug("received message (~s , ~s, ~s)",[_Type, _Event, _CallId]), - wait_for_correlated_message(CallId, Event, Type, kz_time:decr_timeout(Timeout, Start)) - end - end; -wait_for_correlated_message(Call, Event, Type, Timeout) -> - CallId = kapps_call:call_id(Call), - wait_for_correlated_message(CallId, Event, Type, Timeout). diff --git a/core/kazoo_documents/src/kz_api_sms.erl b/core/kazoo_documents/src/kz_api_sms.erl new file mode 100644 index 00000000000..900d8945e28 --- /dev/null +++ b/core/kazoo_documents/src/kz_api_sms.erl @@ -0,0 +1,263 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2010-2019, 2600Hz +%%% @doc This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(kz_api_sms). + +-export([new/0]). +-export([body/1, body/2, set_body/2]). +-export([from/1, from/2, set_from/2]). +-export([from_user/1, from_user/2]). +-export([from_realm/1, from_realm/2]). +-export([scheduled/1, scheduled/2, set_scheduled/2]). +-export([to/1, to/2, set_to/2]). +-export([to_user/1, to_user/2]). +-export([to_realm/1, to_realm/2]). +-export([caller_id_number/1, caller_id_number/2]). +-export([callee_id_number/1, callee_id_number/2]). +-export([account_id/1, account_id/2, set_account_id/2]). +-export([application_id/1, application_id/2]). +-export([route_id/1, route_id/2, set_route_id/2]). +-export([route_type/1, route_type/2, set_route_type/2]). +-export([message_id/1, message_id/2, set_message_id/2]). +-export([exchange_id/1, exchange_id/2, set_exchange_id/2]). + +-export([originator_properties/1 + ,set_originator_properties/2 + ,originator_property/2 + ,set_originator_property/3 + ,remove_originator_property/2 + ,originator_flags/1 + ,set_originator_flags/2 + ,set_originator_flag/2 + ,remove_originator_flag/2 + ]). + +-export([type/0, type/1]). + +-include("kz_documents.hrl"). + +-type payload() :: kz_json:object(). +-export_type([payload/0]). + +-define(SCHEMA, <<"sms">>). +-define(TYPE, <<"sms">>). + +-spec new() -> payload(). +new() -> + kz_json_schema:default_object(?SCHEMA). + +-spec body(payload()) -> kz_term:api_ne_binary(). +body(Payload) -> + body(Payload, 'undefined'). + +-spec body(payload(), Default) -> kz_term:ne_binary() | Default. +body(Payload, Default) -> + kz_json:get_ne_binary_value([<<"Body">>], Payload, Default). + +-spec set_body(payload(), kz_term:ne_binary()) -> payload(). +set_body(Payload, Body) -> + kz_json:set_value([<<"Body">>], Body, Payload). + +-spec from(payload()) -> kz_term:api_binary(). +from(Payload) -> + from(Payload, 'undefined'). + +-spec from(payload(), Default) -> binary() | Default. +from(Payload, Default) -> + kz_json:get_binary_value([<<"From">>], Payload, Default). + +-spec set_from(payload(), binary()) -> payload(). +set_from(Payload, From) -> + kz_json:set_value([<<"From">>], From, Payload). + +-spec scheduled(payload()) -> kz_term:api_integer(). +scheduled(Payload) -> + scheduled(Payload, 'undefined'). + +-spec scheduled(payload(), Default) -> integer() | Default. +scheduled(Payload, Default) -> + kz_json:get_integer_value([<<"Scheduled">>], Payload, Default). + +-spec set_scheduled(payload(), integer()) -> payload(). +set_scheduled(Payload, Scheduled) -> + kz_json:set_value([<<"Scheduled">>], Scheduled, Payload). + +-spec to(payload()) -> kz_term:api_binary(). +to(Payload) -> + to(Payload, 'undefined'). + +-spec to(payload(), Default) -> binary() | Default. +to(Payload, Default) -> + kz_json:get_binary_value([<<"To">>], Payload, Default). + +-spec set_to(payload(), binary()) -> payload(). +set_to(Payload, To) -> + kz_json:set_value([<<"To">>], To, Payload). + +-spec from_user(payload()) -> kz_term:api_binary(). +from_user(Payload) -> + from_user(Payload, 'undefined'). + +-spec from_user(payload(), Default) -> binary() | Default. +from_user(Payload, Default) -> + kz_json:get_binary_value([<<"From-User">>], Payload, Default). + +-spec from_realm(payload()) -> kz_term:api_binary(). +from_realm(Payload) -> + from_realm(Payload, 'undefined'). + +-spec from_realm(payload(), Default) -> binary() | Default. +from_realm(Payload, Default) -> + kz_json:get_binary_value([<<"From-Realm">>], Payload, Default). + +-spec to_user(payload()) -> kz_term:api_binary(). +to_user(Payload) -> + to_user(Payload, 'undefined'). + +-spec to_user(payload(), Default) -> binary() | Default. +to_user(Payload, Default) -> + kz_json:get_binary_value([<<"To-User">>], Payload, Default). + +-spec to_realm(payload()) -> kz_term:api_binary(). +to_realm(Payload) -> + to_realm(Payload, 'undefined'). + +-spec to_realm(payload(), Default) -> binary() | Default. +to_realm(Payload, Default) -> + kz_json:get_binary_value([<<"To-Realm">>], Payload, Default). + +-spec caller_id_number(payload()) -> kz_term:api_binary(). +caller_id_number(Payload) -> + caller_id_number(Payload, 'undefined'). + +-spec caller_id_number(payload(), Default) -> binary() | Default. +caller_id_number(Payload, Default) -> + kz_json:get_binary_value(<<"Caller-ID-Number">>, Payload, Default). + +-spec callee_id_number(payload()) -> kz_term:api_binary(). +callee_id_number(Payload) -> + callee_id_number(Payload, 'undefined'). + +-spec callee_id_number(payload(), Default) -> binary() | Default. +callee_id_number(Payload, Default) -> + kz_json:get_binary_value(<<"Callee-ID-Number">>, Payload, Default). + +-spec account_id(payload()) -> kz_term:api_binary(). +account_id(Payload) -> + account_id(Payload, 'undefined'). + +-spec account_id(payload(), Default) -> binary() | Default. +account_id(Payload, Default) -> + kz_json:get_binary_value(<<"Account-ID">>, Payload, Default). + +-spec set_account_id(payload(), kz_term:ne_binary()) -> payload(). +set_account_id(Payload, AccountId) -> + kz_json:set_value(<<"Account-ID">>, AccountId, Payload). + +-spec application_id(payload()) -> kz_term:api_binary(). +application_id(Payload) -> + application_id(Payload, <<"sms">>). + +-spec application_id(payload(), Default) -> binary() | Default. +application_id(Payload, Default) -> + kz_json:get_binary_value(<<"Application-ID">>, Payload, Default). + +-spec route_id(payload()) -> kz_term:api_binary(). +route_id(Payload) -> + route_id(Payload, 'undefined'). + +-spec route_id(payload(), Default) -> binary() | Default. +route_id(Payload, Default) -> + kz_json:get_binary_value(<<"Route-ID">>, Payload, Default). + +-spec set_route_id(payload(), kz_term:ne_binary()) -> payload(). +set_route_id(Payload, RouteId) -> + kz_json:set_value(<<"Route-ID">>, RouteId, Payload). + +-spec route_type(payload()) -> kz_term:api_binary(). +route_type(Payload) -> + route_type(Payload, 'undefined'). + +-spec route_type(payload(), Default) -> binary() | Default. +route_type(Payload, Default) -> + kz_json:get_binary_value(<<"Route-Type">>, Payload, Default). + +-spec set_route_type(payload(), kz_term:ne_binary()) -> payload(). +set_route_type(Payload, RouteType) -> + kz_json:set_value(<<"Route-Type">>, RouteType, Payload). + +-spec type() -> kz_term:ne_binary(). +type() -> ?TYPE. + +-spec type(payload()) -> kz_term:ne_binary(). +type(Payload) -> + kz_doc:type(Payload, ?TYPE). + +-spec originator_properties(payload()) -> payload(). +originator_properties(Payload) -> + kz_json:get_json_value(<<"Originator-Properties">>, Payload, kz_json:new()). + +-spec set_originator_properties(payload(), kz_json:object()) -> payload(). +set_originator_properties(Payload, JObj) -> + kz_json:set_value(<<"Originator-Properties">>, JObj, Payload). + +-spec originator_property(payload(), kz_term:ne_binary()) -> json_term(). +originator_property(Payload, Key) -> + kz_json:get_value(Key, originator_properties(Payload)). + +-spec set_originator_property(payload(), kz_term:ne_binary(), json_term()) -> payload(). +set_originator_property(Payload, Key, Value) -> + set_originator_properties(Payload, kz_json:set_value(Key, Value, originator_properties(Payload))). + +-spec remove_originator_property(payload(), kz_term:ne_binary()) -> payload(). +remove_originator_property(Payload, Key) -> + JObj = kz_json:set_value(Key, null, originator_properties(Payload)), + case kz_json:is_empty(JObj) of + 'true' -> kz_json:set_value(<<"Originator-Properties">>, null, Payload); + 'false' -> set_originator_properties(Payload, JObj) + end. + +-spec originator_flags(payload()) -> kz_term:ne_binaries(). +originator_flags(Payload) -> + kz_json:get_list_value(<<"Originator-Flags">>, Payload, []). + +-spec set_originator_flags(payload(), kz_term:ne_binaries()) -> payload(). +set_originator_flags(Payload, Flags) -> + kz_json:set_value(<<"Originator-Flags">>, Flags, Payload). + +-spec set_originator_flag(payload(), kz_term:ne_binary()) -> payload(). +set_originator_flag(Payload, Flag) -> + set_originator_flags(Payload, [Flag | originator_flags(Payload)]). + +-spec remove_originator_flag(payload(), kz_term:ne_binary()) -> payload(). +remove_originator_flag(Payload, Flag) -> + set_originator_flags(Payload, lists:filter(fun(F) -> F =/= Flag end, originator_flags(Payload))). + +-spec message_id(payload()) -> kz_term:api_binary(). +message_id(Payload) -> + message_id(Payload, 'undefined'). + +-spec message_id(payload(), Default) -> binary() | Default. +message_id(Payload, Default) -> + kz_json:get_binary_value(<<"Message-ID">>, Payload, Default). + +-spec set_message_id(payload(), kz_term:ne_binary()) -> payload(). +set_message_id(Payload, MessageId) -> + kz_json:set_value(<<"Message-ID">>, MessageId, Payload). + +-spec exchange_id(payload()) -> kz_term:api_binary(). +exchange_id(Payload) -> + exchange_id(Payload, 'undefined'). + +-spec exchange_id(payload(), Default) -> binary() | Default. +exchange_id(Payload, Default) -> + kz_json:get_binary_value(<<"Exchange-ID">>, Payload, Default). + +-spec set_exchange_id(payload(), kz_term:ne_binary()) -> payload(). +set_exchange_id(Payload, ExhangeId) -> + kz_json:set_value(<<"Exchange-ID">>, ExhangeId, Payload). diff --git a/core/kazoo_documents/src/kz_documents.hrl b/core/kazoo_documents/src/kz_documents.hrl index 847b0f3c413..49bdb145095 100644 --- a/core/kazoo_documents/src/kz_documents.hrl +++ b/core/kazoo_documents/src/kz_documents.hrl @@ -2,6 +2,7 @@ -include_lib("kazoo_stdlib/include/kz_types.hrl"). -include_lib("kazoo_stdlib/include/kz_log.hrl"). +-include_lib("kazoo_stdlib/include/kazoo_json.hrl"). -include_lib("kazoo_documents/include/kazoo_documents.hrl"). -define(APP, 'kazoo_documents'). diff --git a/core/kazoo_documents/src/kzd_service_plan.erl b/core/kazoo_documents/src/kzd_service_plan.erl index d5542f92be3..7c4262c08e5 100644 --- a/core/kazoo_documents/src/kzd_service_plan.erl +++ b/core/kazoo_documents/src/kzd_service_plan.erl @@ -40,6 +40,9 @@ -export([asr/1 ,asr/2 ]). +-export([im/1 + ,im/2 + ]). -export([applications/1 ,applications/2 ,set_applications/2 @@ -294,6 +297,17 @@ asr(JObj) -> asr(JObj, Default) -> kz_json:get_ne_json_value(<<"asr">>, JObj, Default). +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec im(doc()) -> kz_json:object(). +im(JObj) -> + im(JObj, kz_json:new()). + +-spec im(doc(), Default) -> Default | kz_json:object(). +im(JObj, Default) -> + kz_json:get_ne_json_value(<<"im">>, JObj, Default). %%------------------------------------------------------------------------------ %% @doc diff --git a/core/kazoo_documents/src/kzd_sms.erl b/core/kazoo_documents/src/kzd_sms.erl index 5f52be103a1..886a5032c59 100644 --- a/core/kazoo_documents/src/kzd_sms.erl +++ b/core/kazoo_documents/src/kzd_sms.erl @@ -19,8 +19,23 @@ -export([caller_id_number/1, caller_id_number/2]). -export([callee_id_number/1, callee_id_number/2]). -export([account_id/1, account_id/2]). +-export([application_id/1, application_id/2]). -export([route_id/1, route_id/2, set_route_id/2]). - +-export([message_id/1, message_id/2, set_message_id/2]). +-export([exchange_id/1, exchange_id/2, set_exchange_id/2]). + +-export([originator_properties/1 + ,set_originator_properties/2 + ,originator_property/2 + ,set_originator_property/3 + ,remove_originator_property/2 + ,originator_flags/1 + ,set_originator_flags/2 + ,set_originator_flag/2 + ,remove_originator_flag/2 + ]). + +-export([type/0, type/1]). -include("kz_documents.hrl"). @@ -28,6 +43,8 @@ -export_type([doc/0]). -define(SCHEMA, <<"sms">>). +-define(TYPE, <<"sms">>). + -spec new() -> doc(). new() -> @@ -153,6 +170,14 @@ account_id(Doc) -> account_id(Doc, Default) -> kz_json:get_binary_value(<<"Account-ID">>, Doc, Default). +-spec application_id(doc()) -> kz_term:api_binary(). +application_id(Doc) -> + application_id(Doc, <<"sms">>). + +-spec application_id(doc(), Default) -> binary() | Default. +application_id(Doc, Default) -> + kz_json:get_binary_value(<<"Application-ID">>, Doc, Default). + -spec route_id(doc()) -> kz_term:api_binary(). route_id(Doc) -> route_id(Doc, 'undefined'). @@ -164,3 +189,75 @@ route_id(Doc, Default) -> -spec set_route_id(doc(), kz_term:ne_binary()) -> doc(). set_route_id(Doc, RouteId) -> kz_json:set_value(<<"Route-ID">>, RouteId, Doc). + +-spec type() -> kz_term:ne_binary(). +type() -> ?TYPE. + +-spec type(doc()) -> kz_term:ne_binary(). +type(Doc) -> + kz_doc:type(Doc, ?TYPE). + +-spec originator_properties(doc()) -> doc(). +originator_properties(Doc) -> + kz_json:get_json_value(<<"Originator-Properties">>, Doc, kz_json:new()). + +-spec set_originator_properties(doc(), kz_json:object()) -> doc(). +set_originator_properties(Doc, JObj) -> + kz_json:set_value(<<"Originator-Properties">>, JObj, Doc). + +-spec originator_property(doc(), kz_term:ne_binary()) -> json_term(). +originator_property(Doc, Key) -> + kz_json:get_value(Key, originator_properties(Doc)). + +-spec set_originator_property(doc(), kz_term:ne_binary(), json_term()) -> doc(). +set_originator_property(Doc, Key, Value) -> + set_originator_properties(Doc, kz_json:set_value(Key, Value, originator_properties(Doc))). + +-spec remove_originator_property(doc(), kz_term:ne_binary()) -> doc(). +remove_originator_property(Doc, Key) -> + JObj = kz_json:set_value(Key, null, originator_properties(Doc)), + case kz_json:is_empty(JObj) of + 'true' -> kz_json:set_value(<<"Originator-Properties">>, null, Doc); + 'false' -> set_originator_properties(Doc, JObj) + end. + +-spec originator_flags(doc()) -> kz_term:ne_binaries(). +originator_flags(Doc) -> + kz_json:get_list_value(<<"Originator-Flags">>, Doc, []). + +-spec set_originator_flags(doc(), kz_term:ne_binaries()) -> doc(). +set_originator_flags(Doc, Flags) -> + kz_json:set_value(<<"Originator-Flags">>, Flags, Doc). + +-spec set_originator_flag(doc(), kz_term:ne_binary()) -> doc(). +set_originator_flag(Doc, Flag) -> + set_originator_flags(Doc, [Flag | originator_flags(Doc)]). + +-spec remove_originator_flag(doc(), kz_term:ne_binary()) -> doc(). +remove_originator_flag(Doc, Flag) -> + set_originator_flags(Doc, lists:filter(fun(F) -> F =/= Flag end, originator_flags(Doc))). + +-spec message_id(doc()) -> kz_term:api_binary(). +message_id(Doc) -> + message_id(Doc, 'undefined'). + +-spec message_id(doc(), Default) -> binary() | Default. +message_id(Doc, Default) -> + kz_json:get_binary_value(<<"Message-ID">>, Doc, Default). + +-spec set_message_id(doc(), kz_term:ne_binary()) -> doc(). +set_message_id(Doc, MessageId) -> + kz_json:set_value(<<"Message-ID">>, MessageId, Doc). + +-spec exchange_id(doc()) -> kz_term:api_binary(). +exchange_id(Doc) -> + exchange_id(Doc, 'undefined'). + +-spec exchange_id(doc(), Default) -> binary() | Default. +exchange_id(Doc, Default) -> + kz_json:get_binary_value(<<"Exchange-ID">>, Doc, Default). + +-spec set_exchange_id(doc(), kz_term:ne_binary()) -> doc(). +set_exchange_id(Doc, ExhangeId) -> + kz_json:set_value(<<"Exchange-ID">>, ExhangeId, Doc). + diff --git a/core/kazoo_im/LICENSE b/core/kazoo_im/LICENSE new file mode 100644 index 00000000000..14e2f777f6c --- /dev/null +++ b/core/kazoo_im/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/core/kazoo_im/Makefile b/core/kazoo_im/Makefile new file mode 100644 index 00000000000..cec38a4515f --- /dev/null +++ b/core/kazoo_im/Makefile @@ -0,0 +1,8 @@ +CWD = $(shell pwd -P) +ROOT = $(realpath $(CWD)/../..) +PROJECT = kazoo_im + +all: compile + +include $(ROOT)/make/kz.mk + diff --git a/core/kazoo_im/deps.mk b/core/kazoo_im/deps.mk new file mode 100644 index 00000000000..e69de29bb2d diff --git a/core/kazoo_im/doc/README.md b/core/kazoo_im/doc/README.md new file mode 100644 index 00000000000..b6934bc904e --- /dev/null +++ b/core/kazoo_im/doc/README.md @@ -0,0 +1,11 @@ +# Kazoo IM + +This application provides functionality related to tracking a IM's metadata, executing commands and routing to external providers. + +## kapps_im + +Record containing information about a IM. Generally populated from `sms_inbound` and `mms_inbound` AMQP payloads. It can be serialized and passed to other Kazoo applications for tranferring control of the IM processing. + +## kapps_im_command + +Set of convenience functions for executing IM commands. A thin wrapper around kapi_sms / kapi_mms with argument defaults, as well as async and sync versions of some command (sync noted by the `b_` prefix for blocking). diff --git a/core/kazoo_im/include/kapps_im_command_types.hrl b/core/kazoo_im/include/kapps_im_command_types.hrl new file mode 100644 index 00000000000..bd12949e936 --- /dev/null +++ b/core/kazoo_im/include/kapps_im_command_types.hrl @@ -0,0 +1,12 @@ +-ifndef(KAPPS_IM_COMMAND_TYPES_HRL). + +-type api_error() :: 'timeout' | + 'not_found' | + kz_json:object(). +-type kapps_api_error() :: {'error', api_error()}. +-type kapps_api_std_return() :: kapps_api_error() | + {'ok', kz_json:object() | kz_term:ne_binary()} | + 'ok'. + +-define(KAPPS_IM_COMMAND_TYPES_HRL, 'true'). +-endif. diff --git a/core/kazoo_im/src/kapps_im.erl b/core/kazoo_im/src/kapps_im.erl new file mode 100644 index 00000000000..17f25145c2e --- /dev/null +++ b/core/kazoo_im/src/kapps_im.erl @@ -0,0 +1,690 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2011-2019, 2600Hz +%%% @doc +%%% @author Karl Anderson +%%% @author James Aimonetti +%%% @author Sponsored by GTNetwork LLC, Implemented by SIPLABS LLC +%%% This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(kapps_im). + +-export([is_im/1]). +-export([new/0]). +-export([from_payload/1, from_payload/2]). +-export([from_sms/1, from_mms/1]). + +-export([put_message_id/1]). + +-export([exec/2]). +-export_type([exec_funs/0]). + +-export([set_application_name/2, application_name/1]). +-export([set_application_version/2, application_version/1]). +-export([set_message_id/2, message_id/1]). + +-export([context/1, context/2, set_context/2]). + +-export([set_controller_queue/2, controller_queue/1]). + +-export([set_request/2, request/1, request_user/1, request_realm/1]). +-export([set_from/2, from/1, from_user/1, from_realm/1]). +-export([set_to/2, to/1, to_user/1, to_realm/1]). + +-export([set_account_id/2, account_id/1]). +-export([account_db/1, account_realm/1]). +-export([set_reseller_id/2, reseller_id/1]). + +-export([set_inception/2, inception/1, inception_type/1]). +-export([is_inter_account/1, inter_account_id/1]). + +-export([set_authorizing_id/2, authorizing_id/1]). +-export([set_authorizing_type/2, authorizing_type/1]). +-export([set_authorization/3]). +-export([set_owner_id/2, owner_id/1]). +-export([set_fetch_id/2, fetch_id/1]). +-export([set_direction/2, direction/1]). +-export([set_route_type/2, route_type/1]). +-export([set_body/2, body/1]). +-export([set_type/2, type/1]). +-export([set_mime/2, mime/1]). +-export([set_endpoint/2, endpoint/1]). + +-export([set_custom_channel_var/3 + ,insert_custom_channel_var/3 + ,set_custom_channel_vars/2 + ,remove_custom_channel_vars/2 + ,update_custom_channel_vars/2 + ,custom_channel_var/3 + ,custom_channel_var/2 + ,custom_channel_vars/1 + ]). + +-export([set_custom_application_var/3 + ,insert_custom_application_var/3 + ,set_custom_application_vars/2 + ,custom_application_var/3 + ,custom_application_var/2 + ,custom_application_vars/1 + ]). + +-export([set_custom_sip_header/3 + ,set_custom_sip_headers/2 + ,custom_sip_header/2, custom_sip_header/3 + ,custom_sip_headers/1 + ]). + +-export([kvs_append/3 + ,kvs_append_list/3 + ,kvs_erase/2 + ,kvs_fetch/2, kvs_fetch/3 + ,kvs_fetch_keys/1 + ,kvs_filter/2 + ,kvs_find/2 + ,kvs_flush/1 + ,kvs_fold/3 + ,kvs_from_proplist/2 + ,kvs_is_key/2 + ,kvs_map/2 + ,kvs_store/3 + ,kvs_store_proplist/2 + ,kvs_to_proplist/1 + ,kvs_update/3 + ,kvs_update/4 + ,kvs_update_counter/3 + ]). + +-include("kazoo_im.hrl"). + +-type im_type() :: 'sms' | 'mms'. +-type direction() :: 'inbound' | 'outbound'. +-type route_type() :: 'onnet' | 'offnet'. + +-record(kapps_im, {message_id :: kz_term:api_binary() + ,context :: kz_term:api_ne_binary() + ,controller_q :: kz_term:api_binary() + ,request :: kz_term:api_ne_binary() + ,request_user :: kz_term:api_ne_binary() + ,request_realm :: kz_term:api_ne_binary() + ,from :: kz_term:api_ne_binary() + ,from_user :: kz_term:api_ne_binary() + ,from_realm :: kz_term:api_ne_binary() + ,to :: kz_term:api_ne_binary() + ,to_user :: kz_term:api_ne_binary() + ,to_realm :: kz_term:api_ne_binary() + ,inception :: kz_term:api_binary() + ,account_id :: kz_term:api_binary() + ,reseller_id :: kz_term:api_binary() + ,authorizing_id :: kz_term:api_binary() + ,authorizing_type :: kz_term:api_binary() + ,owner_id :: kz_term:api_binary() + ,fetch_id :: kz_term:api_binary() + ,app_name = ?APP_NAME :: kz_term:ne_binary() + ,app_version = ?APP_VERSION :: kz_term:ne_binary() + ,ccvs = kz_json:new() :: kz_json:object() + ,cavs = kz_json:new() :: kz_json:object() + ,sip_headers = kz_json:new() :: kz_json:object() + ,kvs = orddict:new() :: orddict:orddict() + ,direction = 'inbound' :: direction() + ,route_type = 'onnet' :: route_type() + ,body :: kz_term:api_binary() + ,type = 'sms' :: im_type() + ,mime = <<"text/plain">> :: kz_term:ne_binary() + ,endpoint = kz_json:new() :: kz_json:object() + }). + +-type im() :: #kapps_im{}. + +-export_type([im/0 + ,im_type/0 + ,route_type/0 + ,direction/0 + ]). + + %-export_type([kapps_api_std_return/0]). + +-define(SPECIAL_VARS, [{<<"Account-ID">>, #kapps_im.account_id} + ,{<<"Reseller-ID">>, #kapps_im.reseller_id} + ,{<<"Authorizing-ID">>, #kapps_im.authorizing_id} + ,{<<"Authorizing-Type">>, #kapps_im.authorizing_type} + ,{<<"Fetch-ID">>, #kapps_im.fetch_id} + ,{<<"Owner-ID">>, #kapps_im.owner_id} + ,{<<"Inception">>, #kapps_im.inception} + ]). + +-spec new() -> im(). +new() -> #kapps_im{}. + +-spec put_message_id(im()) -> kz_term:api_binary(). +put_message_id(#kapps_im{message_id='undefined'}) -> 'undefined'; +put_message_id(#kapps_im{message_id=MsgId}) -> + kz_util:put_callid(MsgId). + +-spec is_im(any()) -> boolean(). +is_im(#kapps_im{}) -> 'true'; +is_im(_) -> 'false'. + +-type exec_fun_1() :: fun((im()) -> im()). +-type exec_fun_2() :: {fun((_, im()) -> im()), _}. +-type exec_fun_3() :: {fun((_, _, im()) -> im()), _, _}. +-type exec_fun() :: exec_fun_1() | exec_fun_2() | exec_fun_3(). +-type exec_funs() :: [exec_fun(),...]. + +-spec exec(exec_funs(), im()) -> im(). +exec(Funs, #kapps_im{}=Im) -> + lists:foldl(fun exec_fold/2, Im, Funs). + +-spec exec_fold(exec_fun(), im()) -> im(). +exec_fold({F, K, V}, C) when is_function(F, 3) -> F(K, V, C); +exec_fold({F, V}, C) when is_function(F, 2) -> F(V, C); +exec_fold(F, C) when is_function(F, 1) -> F(C). + +-spec set_application_name(kz_term:ne_binary(), im()) -> im(). +set_application_name(AppName, #kapps_im{}=Im) when is_binary(AppName) -> + Im#kapps_im{app_name=AppName}. + +-spec application_name(im()) -> kz_term:ne_binary(). +application_name(#kapps_im{app_name=AppName}) -> + AppName. + +-spec set_application_version(kz_term:ne_binary(), im()) -> im(). +set_application_version(AppVersion, #kapps_im{}=Im) when is_binary(AppVersion) -> + Im#kapps_im{app_version=AppVersion}. + +-spec application_version(im()) -> kz_term:ne_binary(). +application_version(#kapps_im{app_version=AppVersion}) -> + AppVersion. + +-spec set_message_id(kz_term:api_binary(), im()) -> im(). +set_message_id(MsgId, #kapps_im{}=Msg) -> + Msg#kapps_im{message_id=MsgId}. + +-spec message_id(im()) -> kz_term:api_binary(). +message_id(#kapps_im{message_id=MsgId}) -> MsgId. + +-spec context(im()) -> kz_term:api_ne_binary(). +context(Im) -> + context(Im, 'undefined'). + +-spec context(im(), Default) -> kz_term:ne_binary() | Default. +context(#kapps_im{context='undefined'}, Default) -> Default; +context(#kapps_im{context=Context}, _Default) -> Context. + +-spec set_context(im(), kz_term:ne_binary()) -> im(). +set_context(#kapps_im{}=Im, Context) -> + Im#kapps_im{context=Context}. + +-spec set_controller_queue(kz_term:ne_binary(), im()) -> im(). +set_controller_queue(ControllerQ, #kapps_im{}=Im) when is_binary(ControllerQ) -> + Im#kapps_im{controller_q=ControllerQ}. + +-spec controller_queue(im()) -> binary(). +controller_queue(#kapps_im{controller_q=ControllerQ}) -> + ControllerQ. + +to_e164(<<"*", _/binary>>=Number, _AccountId) -> Number; +to_e164(Number, 'undefined') -> + knm_converters:normalize(Number); +to_e164(Number, AccountId) -> + knm_converters:normalize(Number, AccountId). + +-spec set_request(kz_term:ne_binary(), im()) -> im(). +set_request(Request, #kapps_im{account_id=AccountId}=Im) when is_binary(Request) -> + [RequestUser, RequestRealm] = binary:split(Request, <<"@">>), + Im#kapps_im{request=Request + ,request_user=to_e164(RequestUser, AccountId) + ,request_realm=RequestRealm + }. + +-spec request(im()) -> kz_term:ne_binary(). +request(#kapps_im{request=Request}) -> + Request. + +-spec request_user(im()) -> kz_term:ne_binary(). +request_user(#kapps_im{request_user=RequestUser}) -> + RequestUser. + +-spec request_realm(im()) -> kz_term:ne_binary(). +request_realm(#kapps_im{request_realm=RequestRealm}) -> + RequestRealm. + +-spec set_from(kz_term:ne_binary(), im()) -> im(). +set_from(From, #kapps_im{}=Im) when is_binary(From) -> + [FromUser, FromRealm] = binary:split(From, <<"@">>), + Im#kapps_im{from=From + ,from_user=FromUser + ,from_realm=FromRealm + }. + +-spec from(im()) -> kz_term:ne_binary(). +from(#kapps_im{from=From}) -> + From. + +-spec from_user(im()) -> kz_term:ne_binary(). +from_user(#kapps_im{from_user=FromUser}) -> + FromUser. + +-spec from_realm(im()) -> kz_term:ne_binary(). +from_realm(#kapps_im{from_realm=FromRealm}) -> + FromRealm. + +-spec set_to(kz_term:ne_binary(), im()) -> im(). +set_to(To, #kapps_im{}=Im) when is_binary(To) -> + [ToUser, ToRealm] = binary:split(To, <<"@">>), + Im#kapps_im{to=To + ,to_user=ToUser + ,to_realm=ToRealm + }. + +-spec to(im()) -> kz_term:ne_binary(). +to(#kapps_im{to=To}) -> + To. + +-spec to_user(im()) -> kz_term:ne_binary(). +to_user(#kapps_im{to_user=ToUser}) -> + ToUser. + +-spec to_realm(im()) -> kz_term:ne_binary(). +to_realm(#kapps_im{to_realm=ToRealm}) -> + ToRealm. + +-spec set_inception(kz_term:api_binary(), im()) -> im(). +set_inception('undefined', #kapps_im{}=Im) -> + Im#kapps_im{inception='undefined'}; +set_inception(Inception, #kapps_im{}=Im) -> + set_custom_channel_var(<<"Inception">>, Inception, Im#kapps_im{inception=Inception}). + +-spec inception(im()) -> kz_term:api_binary(). +inception(#kapps_im{inception=Inception}) -> + Inception. + +-spec account_db(im()) -> kz_term:api_ne_binary(). +account_db(#kapps_im{account_id='undefined'}) -> 'undefined'; +account_db(#kapps_im{account_id=AccountId}) -> kz_util:format_account_db(AccountId). + +-spec set_account_id(kz_term:ne_binary(), im()) -> im(). +set_account_id(<<_/binary>> = AccountId, #kapps_im{}=Im) -> + Props = [{<<"Account-ID">>, AccountId} + ,{<<"Reseller-ID">>, kz_services_reseller:get_id(AccountId)} + ], + set_custom_channel_vars(Props, Im). + +-spec account_id(im()) -> kz_term:api_binary(). +account_id(#kapps_im{account_id=AccountId}) -> + AccountId. + +-spec set_reseller_id(kz_term:ne_binary(), im()) -> im(). +set_reseller_id(<<_/binary>> = ResellerId, #kapps_im{}=Im) -> + Props = [{<<"Reseller-ID">>, ResellerId}], + set_custom_channel_vars(Props, Im). + +-spec reseller_id(im()) -> kz_term:api_binary(). +reseller_id(#kapps_im{reseller_id=ResellerId}) -> + ResellerId. + +-spec account_realm(im()) -> kz_term:ne_binary(). +account_realm(#kapps_im{account_id=AccountId}) -> + {'ok', Doc} = kzd_accounts:fetch(AccountId), + kzd_accounts:realm(Doc). + +-spec set_authorizing_id(kz_term:ne_binary(), im()) -> im(). +set_authorizing_id(AuthorizingId, #kapps_im{}=Im) when is_binary(AuthorizingId) -> + set_custom_channel_var(<<"Authorizing-ID">>, AuthorizingId, Im#kapps_im{authorizing_id=AuthorizingId}). + +-spec authorizing_id(im()) -> kz_term:api_binary(). +authorizing_id(#kapps_im{authorizing_id=AuthorizingId}) -> + AuthorizingId. + +-spec set_authorizing_type(kz_term:ne_binary(), im()) -> im(). +set_authorizing_type(AuthorizingType, #kapps_im{}=Im) when is_binary(AuthorizingType) -> + set_custom_channel_var(<<"Authorizing-Type">>, AuthorizingType, Im#kapps_im{authorizing_type=AuthorizingType}). + +-spec authorizing_type(im()) -> kz_term:api_binary(). +authorizing_type(#kapps_im{authorizing_type=AuthorizingType}) -> + AuthorizingType. + +-spec set_authorization(kz_term:ne_binary(), kz_term:ne_binary(), im()) -> im(). +set_authorization(AuthorizingType, AuthorizingId, #kapps_im{}=Im) + when is_binary(AuthorizingType) + andalso is_binary(AuthorizingId) -> + set_custom_channel_vars([{<<"Authorizing-Type">>, AuthorizingType} + ,{<<"Authorizing-ID">>, AuthorizingId} + ] + ,Im#kapps_im{authorizing_type=AuthorizingType + ,authorizing_id=AuthorizingId + } + ). + +-spec set_owner_id(kz_term:ne_binary(), im()) -> im(). +set_owner_id(OwnerId, #kapps_im{}=Im) when is_binary(OwnerId) -> + set_custom_channel_var(<<"Owner-ID">>, OwnerId, Im#kapps_im{owner_id=OwnerId}). + +-spec owner_id(im()) -> kz_term:api_binary(). +owner_id(#kapps_im{owner_id=OwnerId}) -> OwnerId. + +-spec set_fetch_id(kz_term:ne_binary(), im()) -> im(). +set_fetch_id(FetchId, #kapps_im{}=Im) when is_binary(FetchId) -> + set_custom_channel_var(<<"Fetch-Id">>, FetchId, Im#kapps_im{fetch_id=FetchId}). + +-spec fetch_id(im()) -> kz_term:api_binary(). +fetch_id(#kapps_im{fetch_id=FetchId}) -> FetchId. + +-spec direction(im()) -> direction(). +direction(#kapps_im{direction=Direction}) -> + Direction. + +-spec set_direction(direction() | binary(), im()) -> im(). +set_direction(Direction, #kapps_im{}=Im) + when is_binary(Direction) -> + set_direction(kz_term:to_atom(Direction, 'true'), Im); +set_direction(Direction, #kapps_im{}=Im) + when Direction =:= 'inbound'; + Direction =:= 'outbound' -> + Im#kapps_im{direction=Direction}; +set_direction(_Direction, #kapps_im{}=Im) -> Im. + +-spec route_type(im()) -> route_type(). +route_type(#kapps_im{route_type=Type}) -> + Type. + +-spec set_route_type(route_type() | binary(), im()) -> im(). +set_route_type(Type, #kapps_im{}=Im) + when is_binary(Type)-> + set_route_type(kz_term:to_atom(Type, 'true'), Im); +set_route_type(Type, #kapps_im{}=Im) + when Type =:= 'onnet'; + Type =:= 'offnet' -> + Im#kapps_im{route_type=Type}; +set_route_type(_Type, #kapps_im{}=Im) -> Im. + +-spec body(im()) -> kz_term:ne_binary(). +body(#kapps_im{body=Body}) -> + Body. + +-spec set_body(kz_term:ne_binary(), im()) -> im(). +set_body(Body, #kapps_im{}=Im) when is_binary(Body) -> + Im#kapps_im{body=Body}; +set_body(_Body, #kapps_im{}=Im) -> Im. + +-spec set_endpoint(kz_json:object(), im()) -> im(). +set_endpoint(EP, #kapps_im{}=Im) -> + Im#kapps_im{endpoint=EP}. + +-spec endpoint(im()) -> kz_json:object(). +endpoint(#kapps_im{endpoint=EP}) -> + EP. + +-spec type(im()) -> im_type(). +type(#kapps_im{type=Type}) -> + Type. + +-spec set_type(im_type() | binary(), im()) -> im(). +set_type(Type, #kapps_im{}=Im) + when is_binary(Type) -> + set_type(kz_term:to_atom(Type, 'true'), Im); +set_type(Type, #kapps_im{}=Im) + when Type =:= 'sms'; + Type =:= 'mms' -> + Im#kapps_im{type=Type}; +set_type(_Type, #kapps_im{}=Im) -> Im. + +-spec mime(im()) -> binary(). +mime(#kapps_im{mime=Mime}) -> + Mime. + +-spec set_mime(binary(), im()) -> im(). +set_mime(Mime, #kapps_im{}=Im) -> + Im#kapps_im{mime=Mime}. + +-spec remove_custom_channel_vars(kz_json:keys(), im()) -> im(). +remove_custom_channel_vars(Keys, #kapps_im{}=Im) -> + handle_ccvs_remove(Keys, Im). + +-spec handle_ccvs_remove(kz_json:keys(), im()) -> im(). +handle_ccvs_remove(Keys, #kapps_im{ccvs=CCVs}=Im) -> + lists:foldl(fun ccv_remove_fold/2 + ,Im#kapps_im{ccvs=kz_json:delete_keys(Keys, CCVs)} + ,Keys + ). + +-spec ccv_remove_fold(kz_json:key(), im()) -> im(). +ccv_remove_fold(Key, Im) -> + case props:get_value(Key, ?SPECIAL_VARS) of + 'undefined' -> Im; + Index -> setelement(Index, Im, 'undefined') + end. + +-spec set_custom_channel_var(kz_json:key(), kz_json:json_term(), im()) -> im(). +set_custom_channel_var(Key, Value, Im) -> + insert_custom_channel_var(Key, Value, Im). + +-spec insert_custom_channel_var(kz_json:key(), kz_json:json_term(), im()) -> im(). +insert_custom_channel_var(Key, Value, #kapps_im{ccvs=CCVs}=Im) -> + handle_ccvs_update(kz_json:set_value(Key, Value, CCVs), Im). + +-spec set_custom_channel_vars(kz_term:proplist(), im()) -> im(). +set_custom_channel_vars(Props, #kapps_im{ccvs=CCVs}=Im) -> + NewCCVs = kz_json:set_values(Props, CCVs), + handle_ccvs_update(NewCCVs, Im). + +-spec update_custom_channel_vars([fun((kz_json:object()) -> kz_json:object()),...], im()) -> im(). +update_custom_channel_vars(Updaters, #kapps_im{ccvs=CCVs}=Im) -> + NewCCVs = lists:foldr(fun(F, J) -> F(J) end, CCVs, Updaters), + handle_ccvs_update(NewCCVs, Im). + +-spec custom_channel_var(any(), Default, im()) -> Default | _. +custom_channel_var(Key, Default, #kapps_im{ccvs=CCVs}) -> + kz_json:get_value(Key, CCVs, Default). + +-spec custom_channel_var(any(), im()) -> any(). +custom_channel_var(Key, #kapps_im{ccvs=CCVs}) -> + kz_json:get_value(Key, CCVs). + +-spec custom_channel_vars(im()) -> kz_json:object(). +custom_channel_vars(#kapps_im{ccvs=CCVs}) -> + CCVs. + +-spec set_custom_application_var(kz_json:path(), kz_json:json_term(), im()) -> im(). +set_custom_application_var(Key, Value, Im) -> + set_custom_application_vars([{Key, Value}], Im). + +-spec insert_custom_application_var(kz_json:path(), kz_json:json_term(), im()) -> im(). +insert_custom_application_var(Key, Value, #kapps_im{cavs=CAVs}=Im) -> + Im#kapps_im{cavs=kz_json:set_value(Key, Value, CAVs)}. + +-spec set_custom_application_vars(kz_term:proplist(), im()) -> im(). +set_custom_application_vars(Props, #kapps_im{cavs=CAVs}=Im) -> + NewCAVs = kz_json:set_values(Props, CAVs), + Im#kapps_im{cavs=NewCAVs}. + +-spec custom_application_var(any(), Default, im()) -> Default | _. +custom_application_var(Key, Default, #kapps_im{cavs=CAVs}) -> + kz_json:get_value(Key, CAVs, Default). + +-spec custom_application_var(any(), im()) -> any(). +custom_application_var(Key, #kapps_im{cavs=CAVs}) -> + kz_json:get_value(Key, CAVs). + +-spec custom_application_vars(im()) -> kz_json:object(). +custom_application_vars(#kapps_im{cavs=CAVs}) -> + CAVs. + +-spec set_custom_sip_header(kz_json:path(), kz_json:json_term(), im()) -> im(). +set_custom_sip_header(Key, Value, #kapps_im{sip_headers=SHs}=Im) -> + Im#kapps_im{sip_headers=kz_json:set_value(Key, Value, SHs)}. + +-spec custom_sip_header(kz_json:get_key(), im()) -> kz_json:api_json_term(). +custom_sip_header(Key, #kapps_im{}=Im) -> + custom_sip_header(Key, 'undefined', Im). + +-spec custom_sip_header(kz_json:get_key(), Default, im()) -> kz_json:json_term() | Default. +custom_sip_header(Key, Default, #kapps_im{sip_headers=SHs}) -> + kz_json:get_value(Key, SHs, Default). + +-spec set_custom_sip_headers(kz_term:proplist(), im()) -> im(). +set_custom_sip_headers(Headers, #kapps_im{sip_headers=SHs}=Im) -> + Im#kapps_im{sip_headers=kz_json:set_values(Headers, SHs)}. + +-spec custom_sip_headers(im()) -> kz_json:object(). +custom_sip_headers(#kapps_im{sip_headers=SHs}) -> SHs. + +-spec handle_ccvs_update(kz_json:object(), im()) -> im(). +handle_ccvs_update(CCVs, #kapps_im{}=Im) -> + lists:foldl(fun({Var, Index}, C) -> + case kz_json:get_ne_value(Var, CCVs) of + 'undefined' -> C; + Value -> setelement(Index, C, Value) + end + end + ,Im#kapps_im{ccvs=CCVs} + ,?SPECIAL_VARS + ). + +-spec kvs_append(any(), any(), im()) -> im(). +kvs_append(Key, Value, #kapps_im{kvs=Dict}=Im) -> + Im#kapps_im{kvs=orddict:append(kz_term:to_binary(Key), Value, Dict)}. + +-spec kvs_append_list(any(), [any(),...], im()) -> im(). +kvs_append_list(Key, ValList, #kapps_im{kvs=Dict}=Im) -> + Im#kapps_im{kvs=orddict:append_list(kz_term:to_binary(Key), ValList, Dict)}. + +-spec kvs_erase(any() | [any(),...], im()) -> im(). +kvs_erase(Keys, #kapps_im{kvs=Dict}=Im) when is_list(Keys)-> + Im#kapps_im{kvs=erase_keys(Keys, Dict)}; +kvs_erase(Key, #kapps_im{kvs=Dict}=Im) -> + Im#kapps_im{kvs=erase_key(Key, Dict)}. + +-spec erase_keys(list(), orddict:orddict()) -> orddict:orddict(). +erase_keys(Keys, Dict) -> + lists:foldl(fun erase_key/2, Dict, Keys). + +-spec erase_key(any(), orddict:orddict()) -> orddict:orddict(). +erase_key(K, D) -> orddict:erase(kz_term:to_binary(K), D). + +-spec kvs_flush(im()) -> im(). +kvs_flush(#kapps_im{}=Im) -> Im#kapps_im{kvs=orddict:new()}. + +-spec kvs_fetch(any(), im()) -> any(). +kvs_fetch(Key, Im) -> kvs_fetch(Key, 'undefined', Im). + +-spec kvs_fetch(any(), Default, im()) -> any() | Default. +kvs_fetch(Key, Default, #kapps_im{kvs=Dict}) -> + try orddict:fetch(kz_term:to_binary(Key), Dict) + catch + 'error':'function_clause' -> Default + end. + +-spec kvs_fetch_keys(im()) -> [any(),...]. +kvs_fetch_keys(#kapps_im{kvs=Dict}) -> orddict:fetch_keys(Dict). + +-spec kvs_filter(fun((any(), any()) -> boolean()), im()) -> im(). +kvs_filter(Pred, #kapps_im{kvs=Dict}=Im) -> + Im#kapps_im{kvs=orddict:filter(Pred, Dict)}. + +-spec kvs_find(any(), im()) -> {'ok', any()} | 'error'. +kvs_find(Key, #kapps_im{kvs=Dict}) -> + orddict:find(kz_term:to_binary(Key), Dict). + +-spec kvs_fold(fun((any(), any(), any()) -> any()), any(), im()) -> im(). +kvs_fold(Fun, Acc0, #kapps_im{kvs=Dict}) -> orddict:fold(Fun, Acc0, Dict). + +-spec kvs_from_proplist(kz_term:proplist(), im()) -> im(). +kvs_from_proplist(List, #kapps_im{kvs=Dict}=Im) -> + L = orddict:from_list([{kz_term:to_binary(K), V} || {K, V} <- List]), + Im#kapps_im{kvs=orddict:merge(fun(_, V1, _) -> V1 end, L, Dict)}. + +-spec kvs_is_key(any(), im()) -> boolean(). +kvs_is_key(Key, #kapps_im{kvs=Dict}) -> + orddict:is_key(kz_term:to_binary(Key), Dict). + +-spec kvs_map(fun((any(), any()) -> any()), im()) -> im(). +kvs_map(Pred, #kapps_im{kvs=Dict}=Im) -> + Im#kapps_im{kvs=orddict:map(Pred, Dict)}. + +-spec kvs_store(any(), any(), im()) -> im(). +kvs_store(Key, Value, #kapps_im{kvs=Dict}=Im) -> + Im#kapps_im{kvs=orddict:store(kz_term:to_binary(Key), Value, Dict)}. + +-spec kvs_store_proplist(kz_term:proplist(), im()) -> im(). +kvs_store_proplist(List, #kapps_im{kvs=Dict}=Im) -> + Im#kapps_im{kvs=add_to_store(List, Dict)}. + +add_to_store(List, Dict) -> + lists:foldr(fun add_to_store_fold/2, Dict, List). + +add_to_store_fold({K, V}, D) -> + orddict:store(kz_term:to_binary(K), V, D). + +-spec kvs_to_proplist(im()) -> kz_term:proplist(). +kvs_to_proplist(#kapps_im{kvs=Dict}) -> + orddict:to_list(Dict). + +-spec kvs_update(any(), fun((any()) -> any()), im()) -> im(). +kvs_update(Key, Fun, #kapps_im{kvs=Dict}=Im) -> + Im#kapps_im{kvs=orddict:update(kz_term:to_binary(Key), Fun, Dict)}. + +-spec kvs_update(any(), fun((any()) -> any()), any(), im()) -> im(). +kvs_update(Key, Fun, Initial, #kapps_im{kvs=Dict}=Im) -> + Im#kapps_im{kvs=orddict:update(kz_term:to_binary(Key), Fun, Initial, Dict)}. + +-spec kvs_update_counter(any(), number(), im()) -> im(). +kvs_update_counter(Key, Number, #kapps_im{kvs=Dict}=Im) -> + Im#kapps_im{kvs=orddict:update_counter(kz_term:to_binary(Key), Number, Dict)}. + +-spec inception_type(im()) -> kz_term:ne_binary(). +inception_type(#kapps_im{inception='undefined'}) -> <<"onnet">>; +inception_type(#kapps_im{}) -> <<"offnet">>. + +-spec is_inter_account(im()) -> boolean(). +is_inter_account(#kapps_im{}=Im) -> + inter_account_id(Im) /= 'undefined'. + +-spec inter_account_id(im()) -> kz_term:api_binary(). +inter_account_id(#kapps_im{}=Im) -> + custom_channel_var(<<"Inception-Account-ID">>, Im). + +-spec from_sms(kz_json:object()) -> im(). +from_sms(SmsReq) -> + from_payload(SmsReq, new()). + +-spec from_mms(kz_json:object()) -> im(). +from_mms(SmsReq) -> + from_payload(SmsReq, set_type('mms', new())). + +-spec from_payload(kz_json:object()) -> im(). +from_payload(SmsReq) -> + from_payload(SmsReq, new()). + +-spec from_payload(kz_json:object(), im()) -> im(). +from_payload(SmsReq, IM) -> + MessageId = kz_api_sms:message_id(SmsReq, kz_binary:rand_hex(16)), + CCVs = kz_json:to_proplist(<<"Custom-Channel-Vars">>, SmsReq), + From = kz_api_sms:from(SmsReq), + To = kz_api_sms:to(SmsReq), + AccountId = kz_api_sms:account_id(SmsReq), + Realm = kzd_accounts:fetch_realm(AccountId), + + Routines = [{fun set_message_id/2, MessageId} + ,{fun set_direction/2, kz_api:event_name(SmsReq)} + ,{fun set_account_id/2, AccountId} + ,{fun set_authorizing_id/2, AccountId} + ,{fun set_custom_channel_vars/2, CCVs} + ,{fun set_from/2, <>} + ,{fun set_request/2, <>} + ,{fun set_to/2, <>} + ,{fun set_application_name/2, ?APP_NAME} + ,{fun set_application_version/2, ?APP_VERSION} + ,{fun set_body/2, kz_api_sms:body(SmsReq)} + ,fun fetch_endpoint/1 + ], + exec(Routines, IM). + +-spec fetch_endpoint(im()) -> im(). +fetch_endpoint(Im) -> + case kz_endpoint:get(authorizing_id(Im), account_id(Im)) of + {'ok', JObj} -> set_endpoint(JObj, Im); + _Else -> Im + end. diff --git a/core/kazoo_im/src/kapps_im_command.erl b/core/kazoo_im/src/kapps_im_command.erl new file mode 100644 index 00000000000..84b6b58e4ad --- /dev/null +++ b/core/kazoo_im/src/kapps_im_command.erl @@ -0,0 +1,266 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2012-2019, 2600Hz +%%% @doc +%%% @author Karl Anderson +%%% @author James Aimonetti +%%% This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(kapps_im_command). + +-include("kapps_im_command.hrl"). + +-type im() :: kapps_im:im(). + +-export([send_sms/2, send_sms/3, send_sms/4]). + +-export([relay_event/2, relay_event/3]). + +-export([default_collect_timeout/0 + ,default_message_timeout/0 + ,default_application_timeout/0 + ]). + +-define(CONFIG_CAT, <<"sms_command">>). + +-define(DEFAULT_COLLECT_TIMEOUT + ,kapps_config:get_integer(?CONFIG_CAT, <<"collect_timeout">>, 60 * ?MILLISECONDS_IN_SECOND) + ). + +-define(DEFAULT_MESSAGE_TIMEOUT, kapps_config:get_integer(?CONFIG_CAT, <<"message_timeout">>, 60 * ?MILLISECONDS_IN_SECOND)). + +-define(DEFAULT_APPLICATION_TIMEOUT + ,kapps_config:get_integer(?CONFIG_CAT, <<"application_timeout">>, 500 * ?MILLISECONDS_IN_SECOND) + ). +-define(DEFAULT_STRATEGY, <<"single">>). + +-define(ATOM(X), kz_term:to_atom(X, 'true')). +-define(SMS_POOL(A,B,C), ?ATOM(<>) ). + +-define(SMS_DEFAULT_OUTBOUND_OPTIONS + ,kz_json:from_list([{<<"delivery_mode">>, 2} + ,{<<"mandatory">>, 'true'} + ]) + ). +-define(SMS_OUTBOUND_OPTIONS_KEY, [<<"outbound">>, <<"options">>]). +-define(SMS_OUTBOUND_OPTIONS + ,kapps_config:get_json(<<"sms">>, ?SMS_OUTBOUND_OPTIONS_KEY, ?SMS_DEFAULT_OUTBOUND_OPTIONS) + ). + +-spec default_collect_timeout() -> pos_integer(). +default_collect_timeout() -> + ?DEFAULT_COLLECT_TIMEOUT. + +-spec default_message_timeout() -> pos_integer(). +default_message_timeout() -> + ?DEFAULT_MESSAGE_TIMEOUT. + +-spec default_application_timeout() -> pos_integer(). +default_application_timeout() -> + ?DEFAULT_APPLICATION_TIMEOUT. + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec send_sms(kz_json:objects(), im()) -> kapps_api_std_return(). +send_sms(Endpoints, Im) -> send_sms(Endpoints, ?DEFAULT_STRATEGY, Im). + +-spec send_sms(kz_json:objects(), binary(), im()) -> kapps_api_std_return(). +send_sms(EndpointList, Strategy, Im) -> send_sms(EndpointList, Strategy, default_message_timeout(), Im). + +-spec send_sms(kz_json:objects(), binary(), integer(), im()) -> kapps_api_std_return(). +send_sms(EndpointList, Strategy, Timeout, Im) -> + Endpoints = create_sms_endpoints(EndpointList, []), + API = create_sms(Im), + send_and_wait(Strategy, API, Endpoints, Timeout). + +-spec send_and_wait(binary(), kz_term:proplist(), kz_json:objects(), integer()) -> kapps_api_std_return(). +send_and_wait(<<"single">>, _API, [], _Timeout) -> + {'error', 'no endpoints available'}; +send_and_wait(<<"single">>, API, [Endpoint| Others], Timeout) -> + case send_and_wait(API, Endpoint, Timeout) of + {'error', _R}=Err when Others =:= [] -> + Err; + {'error', _R} -> + send_and_wait(<<"single">>, API, Others, Timeout); + {_, JObj} = Ret -> + lager:info("received ~s ~s", [kz_api:event_category(JObj), kz_api:event_name(JObj)]), + Ret + end; +send_and_wait(_Strategy, _API, _Endpoints, _Timeout) -> + {'error', 'strategy_not_implemented'}. + +-spec send_and_wait(kz_term:proplist(), kz_json:object(), integer()) -> kapps_api_std_return(). +send_and_wait(API, Endpoint, Timeout) -> + Options = kz_json:to_proplist(kz_json:get_value(<<"Endpoint-Options">>, Endpoint, [])), + Payload = props:set_values( [{<<"Endpoints">>, [Endpoint]} | Options], API), + MsgId = props:get_value(<<"Message-ID">>, Payload), + lager:info("sending sms and waiting for response"), + _ = kapi_sms:publish_message(Payload), + wait_for_correlated_message(MsgId, <<"delivery">>, <<"message">>, Timeout). + +-spec create_sms(im()) -> kz_term:proplist(). +create_sms(Im) -> + AccountId = kapps_im:account_id(Im), + AccountRealm = kapps_im:to_realm(Im), + CCVUpdates = props:filter_undefined( + [{<<"Account-ID">>, AccountId} + ,{<<"Account-Realm">>, AccountRealm} + ,{<<"From-User">>, kapps_im:from_user(Im)} + ,{<<"From-Realm">>, kapps_im:from_realm(Im)} + ,{<<"From-URI">>, kapps_im:from(Im)} + ,{<<"Reseller-ID">>, kz_services_reseller:get_id(AccountId)} + ]), + [{<<"Message-ID">>, kapps_im:message_id(Im)} + ,{<<"Call-ID">>, kapps_im:message_id(Im)} + ,{<<"Body">>, kapps_im:body(Im)} + ,{<<"From">>, kapps_im:from(Im)} + ,{<<"To">>, kapps_im:to(Im)} + ,{<<"Request">>, kapps_im:request(Im) } + ,{<<"Application-Name">>, <<"send">>} + ,{<<"Custom-Channel-Vars">>, kz_json:set_values(CCVUpdates, kz_json:new())} + | kz_api:default_headers(kapps_im:controller_queue(Im), ?APP_NAME, ?APP_VERSION) + ]. + +-spec create_sms_endpoints(kz_json:objects(), kz_json:objects()) -> kz_json:objects(). +create_sms_endpoints([], Endpoints) -> Endpoints; +create_sms_endpoints([Endpoint | Others], Endpoints) -> + case create_sms_endpoint(Endpoint) of + 'undefined' -> create_sms_endpoints(Others, Endpoints); + NewEndpoint -> create_sms_endpoints(Others, [NewEndpoint | Endpoints]) + end. + +-spec create_sms_endpoint(kz_json:object()) -> kz_term:api_object(). +create_sms_endpoint(Endpoint) -> + Realm = kz_json:get_value(<<"To-Realm">>, Endpoint), + Username = kz_json:get_value(<<"To-Username">>, Endpoint), + case lookup_reg(Username, Realm) of + {'ok', Node} -> + Options = kz_json:get_value(<<"Endpoint-Options">>, Endpoint, []), + kz_json:set_values( + [{<<"Route-ID">>, Node} + ,{<<"Endpoint-Options">>, kz_json:from_list([{<<"Route-ID">>, Node} | Options])} + ], Endpoint); + {'error', _E} -> 'undefined' + end. + +-spec lookup_reg(kz_term:ne_binary(), kz_term:ne_binary()) -> {'error', any()} | + {'ok', kz_term:ne_binary()}. +lookup_reg('undefined', _Realm) -> {'error', 'invalid_user'}; +lookup_reg(_Username, 'undefined') -> {'error', 'invalid_realm'}; +lookup_reg(Username, Realm) -> + Req = [{<<"Realm">>, Realm} + ,{<<"Username">>, Username} + ,{<<"Fields">>, [<<"Registrar-Node">>]} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ], + case kz_amqp_worker:call_collect(Req + ,fun kapi_registration:publish_query_req/1 + ,{'ecallmgr', 'true'} + ) + of + {'error', _E}=E -> + lager:debug("error getting registration: ~p", [_E]), + E; + {_, JObjs} -> + case extract_device_registrations(JObjs) of + [] -> {'error', 'not_registered'}; + [FirstNode | _Others] -> {'ok', FirstNode} + end + end. + +-spec extract_device_registrations(kz_json:objects()) -> kz_term:ne_binaries(). +extract_device_registrations(JObjs) -> + sets:to_list(extract_device_registrations(JObjs, sets:new())). + +-spec extract_device_registrations(kz_json:objects(), sets:set()) -> sets:set(). +extract_device_registrations([], Set) -> Set; +extract_device_registrations([JObj|JObjs], Set) -> + Fields = kz_json:get_value(<<"Fields">>, JObj, []), + S = lists:foldl(fun extract_device_registrar_fold/2, Set, Fields), + extract_device_registrations(JObjs, S). + +-spec extract_device_registrar_fold(kz_json:object(), sets:set()) -> sets:set(). +extract_device_registrar_fold(JObj, Set) -> + case kz_json:get_ne_value(<<"Registrar-Node">>, JObj) of + 'undefined' -> Set; + AuthId -> sets:add_element(AuthId, Set) + end. + +-spec get_correlated_msg_type(kz_json:object()) -> + {kz_term:api_binary(), kz_term:api_binary(), kz_term:api_binary()}. +get_correlated_msg_type(JObj) -> + get_correlated_msg_type(<<"Message-ID">>, JObj). + +-spec get_correlated_msg_type(kz_term:ne_binary(), kz_json:object()) -> + {kz_term:api_binary(), kz_term:api_binary(), kz_term:api_binary()}. +get_correlated_msg_type(Key, JObj) -> + {C, N} = kz_util:get_event_type(JObj), + {C, N, kz_json:get_value(Key, JObj)}. + +-spec wait_for_correlated_message(kz_term:ne_binary() | im(), kz_term:ne_binary(), kz_term:ne_binary(), timeout()) -> + kapps_api_std_return(). +wait_for_correlated_message(MsgId, Event, Type, Timeout) + when is_binary(MsgId) -> + Start = os:timestamp(), + case receive_event(Timeout) of + {'error', 'timeout'}=E -> E; + {'ok', JObj}=Ok -> + case get_correlated_msg_type(JObj) of + {<<"error">>, _, MsgId} -> + lager:debug("message execution error while waiting for ~s", [MsgId]), + {'error', JObj}; + {Type, Event, MsgId} -> + Ok; + {_Type, _Event, _MsgId} -> + lager:debug("received message (~s , ~s, ~s)",[_Type, _Event, _MsgId]), + wait_for_correlated_message(MsgId, Event, Type, kz_time:decr_timeout(Timeout, Start)) + end + end; +wait_for_correlated_message(Im, Event, Type, Timeout) -> + MsgId = kapps_im:message_id(Im), + wait_for_correlated_message(MsgId, Event, Type, Timeout). + +-type received_event() :: {'ok', kz_json:object()} | + {'error', 'timeout'}. + +-spec receive_event(timeout()) -> received_event(). +receive_event(Timeout) -> receive_event(Timeout, 'true'). + +-spec receive_event(timeout(), boolean()) -> + received_event() | + {'other', kz_json:object() | any()}. +receive_event(T, _) when T =< 0 -> {'error', 'timeout'}; +receive_event(Timeout, IgnoreOthers) -> + Start = os:timestamp(), + receive + {'amqp_msg', JObj} -> {'ok', JObj}; + {'kapi',{ _, _, JObj}} -> {'ok', JObj}; + _Msg when IgnoreOthers -> + lager:debug_unsafe("ignoring received event : ~p", [_Msg]), + receive_event(kz_time:decr_timeout(Timeout, Start), IgnoreOthers); + Other -> + lager:debug_unsafe("received other event : ~p", [Other]), + {'other', Other} + after + Timeout -> {'error', 'timeout'} + end. + +%%------------------------------------------------------------------------------ +%% @doc How AMQP messages are sent to the mailboxes of processes waiting +%% for them in the receive blocks below. +%% @end +%%------------------------------------------------------------------------------ +-type relay_fun() :: fun((pid() | atom(), any()) -> any()). + +-spec relay_event(pid(), kz_json:object()) -> any(). +relay_event(Pid, JObj) -> + relay_event(Pid, JObj, fun erlang:send/2). + +-spec relay_event(pid(), kz_json:object(), relay_fun()) -> any(). +relay_event(Pid, JObj, RelayFun) -> + RelayFun(Pid, {'amqp_msg', JObj}). diff --git a/core/kazoo_call/src/kapps_sms_command.hrl b/core/kazoo_im/src/kapps_im_command.hrl similarity index 53% rename from core/kazoo_call/src/kapps_sms_command.hrl rename to core/kazoo_im/src/kapps_im_command.hrl index 92a65ab7925..fe1c73435a9 100644 --- a/core/kazoo_call/src/kapps_sms_command.hrl +++ b/core/kazoo_im/src/kapps_im_command.hrl @@ -1,14 +1,14 @@ --ifndef(KAPPS_SMS_COMMAND_HRL). +-ifndef(KAPPS_IM_COMMAND_HRL). -include_lib("kazoo_stdlib/include/kz_types.hrl"). -include_lib("kazoo_stdlib/include/kz_log.hrl"). --include("kapps_call_command_types.hrl"). +-include("kapps_im_command_types.hrl"). -define(DEFAULT_TIMEOUT_S, 20). --define(APP_NAME, <<"kapps_sms_command">>). +-define(APP_NAME, <<"kapps_im_command">>). -define(APP_VERSION, <<"4.0.0">>). --define(KAPPS_SMS_COMMAND_HRL, 'true'). +-define(KAPPS_IM_COMMAND_HRL, 'true'). -endif. diff --git a/core/kazoo_im/src/kazoo_im.app.src b/core/kazoo_im/src/kazoo_im.app.src new file mode 100644 index 00000000000..0446cd16045 --- /dev/null +++ b/core/kazoo_im/src/kazoo_im.app.src @@ -0,0 +1,13 @@ +{application,kazoo_im, + [{applications,[crypto,kazoo,kazoo_amqp, + kazoo_documents, + kazoo_ledgers, + kazoo_number_manager,kazoo_stdlib, + lager,stdlib]}, + {description,"Representing IM in Kazoo"}, + {id,[]}, + {vsn,"4.0.0"}, + {modules,[]}, + {registered,[]}, + {env,[]}, + {mod,{kazoo_im_app,[]}}]}. diff --git a/core/kazoo_im/src/kazoo_im.hrl b/core/kazoo_im/src/kazoo_im.hrl new file mode 100644 index 00000000000..45eb7d5d73b --- /dev/null +++ b/core/kazoo_im/src/kazoo_im.hrl @@ -0,0 +1,52 @@ +-ifndef(KAZOO_IM_HRL). + +-include_lib("kazoo_stdlib/include/kz_types.hrl"). +-include_lib("kazoo_stdlib/include/kz_log.hrl"). +-include_lib("kazoo_stdlib/include/kz_databases.hrl"). +-include_lib("kazoo/include/kz_api_literals.hrl"). +-include_lib("kazoo_amqp/include/kz_amqp.hrl"). +-include_lib("kazoo_number_manager/include/knm_phone_number.hrl"). + +-define(APP_NAME, <<"kazoo_im">>). +-define(APP_VERSION, <<"4.0.0">>). +-define(CONFIG_CAT, ?APP_NAME). + +-define(CCV(Key), [<<"Custom-Channel-Vars">>, Key]). + +-record(amqp_listener_connection, {name :: binary() + ,broker :: binary() + ,exchange :: binary() + ,type :: binary() + ,queue :: binary() + ,options :: kz_term:proplist() + }). + +-type amqp_listener_connection() :: #amqp_listener_connection{}. +-type amqp_listener_connections() :: [amqp_listener_connection(),...]. + +-define(ATOM(X), kz_term:to_atom(X, 'true')). +-define(APP, ?ATOM(?APP_NAME)). + +-define(RESOURCE_TYPES_HANDLED,[<<"sms">>]). + +-define(DEFAULT_EXCHANGE, <<"sms">>). +-define(DEFAULT_EXCHANGE_TYPE, <<"topic">>). +-define(DEFAULT_EXCHANGE_OPTIONS, [{<<"passive">>, 'true'}] ). +-define(DEFAULT_EXCHANGE_OPTIONS_JOBJ, kz_json:from_list(?DEFAULT_EXCHANGE_OPTIONS) ). +-define(DEFAULT_BROKER, kz_amqp_connections:primary_broker()). +-define(DEFAULT_QUEUE_NAME, <<"smsc_inbound_queue_sms">>). + +-ifdef(OTP_RELEASE). +%% >= OTP 21 +-define(CATCH(Type, Reason, Stacktrace), Type:Reason:Stacktrace). +-define(LOGSTACK(Stacktrace), kz_util:log_stacktrace(Stacktrace)). +-else. +%% =< OTP 20 +-define(CATCH(Type, Reason, Stacktrace), Type:Reason). +-define(LOGSTACK(Stacktrace), kz_util:log_stacktrace()). +-endif. + + +-define(KAZOO_IM_HRL, 'true'). +-endif. + diff --git a/core/kazoo_im/src/kazoo_im_app.erl b/core/kazoo_im/src/kazoo_im_app.erl new file mode 100644 index 00000000000..65713e33b59 --- /dev/null +++ b/core/kazoo_im/src/kazoo_im_app.erl @@ -0,0 +1,34 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2010-2019, 2600Hz +%%% @doc +%%% @author Karl Anderson +%%% This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(kazoo_im_app). +-behaviour(application). + +-include_lib("kazoo_stdlib/include/kz_types.hrl"). + +-export([start/2, stop/1]). + +%% Application callbacks + +%%------------------------------------------------------------------------------ +%% @doc Implement the application start behaviour. +%% @end +%%------------------------------------------------------------------------------ +-spec start(application:start_type(), any()) -> kz_types:startapp_ret(). +start(_StartType, _StartArgs) -> + kazoo_im_sup:start_link(). + +%%------------------------------------------------------------------------------ +%% @doc Implement the application stop behaviour. +%% @end +%%------------------------------------------------------------------------------ +-spec stop(any()) -> any(). +stop(_State) -> + 'ok'. diff --git a/applications/doodle/src/doodle_event_handler_sup.erl b/core/kazoo_im/src/kazoo_im_sup.erl similarity index 72% rename from applications/doodle/src/doodle_event_handler_sup.erl rename to core/kazoo_im/src/kazoo_im_sup.erl index ff8d4027cdc..65f92d16d10 100644 --- a/applications/doodle/src/doodle_event_handler_sup.erl +++ b/core/kazoo_im/src/kazoo_im_sup.erl @@ -1,26 +1,26 @@ %%%----------------------------------------------------------------------------- -%%% @copyright (C) 2010-2019, 2600Hz -%%% @doc -%%% @author James Aimonetti +%%% @copyright (C) 2012-2019, 2600Hz +%%% @doc This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% %%% @end %%%----------------------------------------------------------------------------- --module(doodle_event_handler_sup). - +-module(kazoo_im_sup). -behaviour(supervisor). --include("doodle.hrl"). - --define(SERVER, ?MODULE). +-export([start_link/0 + ,init/1 + ]). -%% API --export([start_link/0]). --export([new/3]). --export([workers/0]). +-include_lib("kazoo_stdlib/include/kz_types.hrl"). +-include("kapps_im_command.hrl"). -%% Supervisor callbacks --export([init/1]). +-define(SERVER, ?MODULE). --define(CHILDREN, []). +-define(CHILDREN, [?SUPER('kz_im_offnet_sup') + ,?SUPER('kz_im_onnet_sup') + ]). %%============================================================================== %% API functions @@ -34,14 +34,6 @@ start_link() -> supervisor:start_link({'local', ?SERVER}, ?MODULE, []). --spec new(any(), atom(), list()) -> kz_types:sup_startchild_ret(). -new(Name, M, A) -> - supervisor:start_child(?SERVER, ?WORKER_NAME_ARGS_TYPE(Name, M, A, 'temporary')). - --spec workers() -> kz_term:pids(). -workers() -> - [Pid || {_, Pid, 'worker', [_]} <- supervisor:which_children(?SERVER)]. - %%============================================================================== %% Supervisor callbacks %%============================================================================== @@ -56,7 +48,7 @@ workers() -> -spec init(any()) -> kz_types:sup_init_ret(). init([]) -> RestartStrategy = 'one_for_one', - MaxRestarts = 0, + MaxRestarts = 25, MaxSecondsBetweenRestarts = 1, SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts}, diff --git a/core/kazoo_im/src/kz_im_flat_rate.erl b/core/kazoo_im/src/kz_im_flat_rate.erl new file mode 100644 index 00000000000..d4c1393403e --- /dev/null +++ b/core/kazoo_im/src/kz_im_flat_rate.erl @@ -0,0 +1,87 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2010-2019, 2600Hz +%%% @doc This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(kz_im_flat_rate). + +-include("kazoo_im.hrl"). + +-export([debit/1]). + +-define(IM_LEDGER, <<"im">>). + +-type ledger_result() :: {'ok', kz_ledger:ledger()} | {'error', any()}. +-type ledgers_result() :: {{'account' | 'reseller', kz_term:ne_binary()}, ledger_result()}. +-type ledgers_results() :: [ledgers_result()]. + +%%%------------------------------------------------------------------------------ +%%% @doc create a ledger entry for the account and the Reseller +%%% @end +%%%------------------------------------------------------------------------------ +-spec create_ledgers(kapps_im:im()) -> ledgers_results(). +create_ledgers(IM) -> + Ids = [{'account', kapps_im:account_id(IM)} + ,{'reseller', kapps_im:reseller_id(IM)} + ], + [{Id, create_ledger(IM, AccountId)} || {_, AccountId} = Id <- Ids]. + +-spec create_ledger(kapps_im:im(), kz_term:api_ne_binary()) -> ledger_result(). +create_ledger(IM, AccountId) -> + Rate = kz_services_im:flat_rate(AccountId, kapps_im:type(IM), kapps_im:direction(IM)), + Setters = + props:filter_empty( + [{fun kz_ledger:set_account/2, AccountId} + ,{fun kz_ledger:set_source_service/2, ?IM_LEDGER} + ,{fun kz_ledger:set_source_id/2, kapps_im:message_id(IM)} + ,{fun kz_ledger:set_description/2, description(IM)} + ,{fun kz_ledger:set_usage_type/2, kz_term:to_binary(kapps_im:type(IM))} + ,{fun kz_ledger:set_usage_quantity/2, 1} + ,{fun kz_ledger:set_usage_unit/2, <<"message">>} + ,{fun kz_ledger:set_period_start/2, kz_time:now_s()} + ,{fun kz_ledger:set_metadata/2, metadata(IM, AccountId)} + ,{fun kz_ledger:set_dollar_amount/2, Rate} + ] + ), + kz_ledger:debit(kz_ledger:setters(Setters), AccountId). + +-spec description(kapps_im:im()) -> kz_term:ne_binary(). +description(IM) -> + list_to_binary([kz_term:to_binary(kapps_im:direction(IM)) + ," from " + ,kapps_im:from_user(IM) + ," to " + ,kapps_im:to_user(IM) + ]). + +%%%------------------------------------------------------------------------------ +%%% @doc +%%% @end +%%%------------------------------------------------------------------------------ +-spec debit(kapps_im:im()) -> 'ok'. +debit(IM) -> + case lists:filter(fun filter_ledger_error/1, create_ledgers(IM)) of + [] -> 'ok'; + Errors -> hd([lager:error("failed to create ~s/~s ledger => ~p", [T, Id, Error]) + || {{T, Id}, {'error', Error}} <- Errors]) + end. + +filter_ledger_error({_, {'error', _}}) -> 'true'; +filter_ledger_error(_) -> 'false'. + +%%%------------------------------------------------------------------------------ +%%% @doc +%%% @end +%%%------------------------------------------------------------------------------ +-spec metadata(kapps_im:im(), kz_term:ne_binary()) -> kz_json:object(). +metadata(IM, AccountId) -> + add_metadata(kapps_im:account_id(IM), AccountId). + +-spec add_metadata(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_json:object(). +add_metadata(AccountId, AccountId) -> + kz_json:new(); +add_metadata(AccountId, _AccountId) -> + kz_json:from_list([{<<"account_id">>, AccountId}]). diff --git a/core/kazoo_im/src/kz_im_offnet.erl b/core/kazoo_im/src/kz_im_offnet.erl new file mode 100644 index 00000000000..2d7130ee66a --- /dev/null +++ b/core/kazoo_im/src/kz_im_offnet.erl @@ -0,0 +1,385 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2013-2019, 2600Hz +%%% @doc This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(kz_im_offnet). +-behaviour(gen_listener). + +-export([start_link/1]). + +-export([route/1, route/2]). + +-export([handle_message/2]). + +-export([init/1 + ,handle_call/3 + ,handle_cast/2 + ,handle_info/2 + ,handle_event/2 + ,terminate/2 + ,code_change/3 + ]). + +-include("kazoo_im.hrl"). + +-define(SERVER, ?MODULE). + +-record(state, {connection :: amqp_listener_connection() + ,confirms = #{count => 1, pids => #{}} :: map() + }). +-type state() :: #state{}. + +-define(BINDINGS(Ex), [{'sms', [{'exchange', Ex} + ,{'restrict_to', ['inbound']} + ]} + ]). +-define(RESPONDERS, [{{?MODULE, 'handle_message'} + ,[{<<"message">>, <<"inbound">>}] + } + ]). + +-define(QUEUE_OPTIONS, [{'exclusive', 'false'} + ,{'durable', 'true'} + ,{'auto_delete', 'false'} + ,{'arguments', [{<<"x-message-ttl">>, 'infinity'} + ,{<<"x-max-length">>, 'infinity'} + ]} + ]). +-define(CONSUME_OPTIONS, [{'exclusive', 'false'} + ,{'no_ack', 'false'} + ]). + +-define(AMQP_PUBLISH_OPTIONS, [{'mandatory', 'true'} + ,{'delivery_mode', 2} + ]). + +-define(ROUTE_TIMEOUT, 'infinity'). + +%%%============================================================================= +%%% API +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc Starts the server. +%% @end +%%------------------------------------------------------------------------------ +-spec start_link(amqp_listener_connection()) -> kz_types:startlink_ret(). +start_link(#amqp_listener_connection{broker=Broker + ,exchange=Exchange + ,type=Type + ,queue=Queue + ,options=Options + }=C) -> + Exchanges = [{Exchange, Type, Options}], + gen_listener:start_link(?SERVER + ,[{'bindings', ?BINDINGS(Exchange)} + ,{'responders', ?RESPONDERS} + ,{'queue_name', Queue} % optional to include + ,{'queue_options', ?QUEUE_OPTIONS} % optional to include + ,{'consume_options', ?CONSUME_OPTIONS} % optional to include + ,{'declare_exchanges', Exchanges} + ,{'broker', Broker} + ,{'server_confirms', 'true'} + ] + ,[C] + ). + +-spec handle_message(kz_json:object(), kz_term:proplist()) -> 'ok'. +handle_message(JObj, Props) -> + Srv = props:get_value('server', Props), + Deliver = props:get_value('deliver', Props), + case kapi_sms:inbound_v(JObj) of + 'true' -> + handle_inbound(JObj, Srv, Deliver); + 'false' -> + lager:debug("error validating inbound message : ~p", [JObj]), + gen_listener:ack(Srv, Deliver) + end. + +-spec route(kz_term:api_terms()) -> 'ok' | {'error', 'no_connections'}. +route(Payload) -> + case kz_im_offnet_sup:worker() of + {'error', 'no_connections'} = E -> E; + Pid -> route(Pid, Payload) + end. + +-spec route(pid(), kz_term:api_terms()) -> 'ok'. +route(Pid, Payload) -> + gen_listener:call(Pid, {'route', Payload}, ?ROUTE_TIMEOUT). + +%%%============================================================================= +%%% gen_server callbacks +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc Initializes the server. +%% @end +%%------------------------------------------------------------------------------ +-spec init([amqp_listener_connection()]) -> {'ok', state()}. +init([#amqp_listener_connection{}=Connection]) -> + {'ok', #state{connection=Connection}}. + +%%------------------------------------------------------------------------------ +%% @doc Handling call messages. +%% @end +%%------------------------------------------------------------------------------ +-spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_types:handle_call_ret_state(state()). +handle_call({'route', Payload}, From, #state{connection = Connection, confirms = Confirms} = State) -> + #amqp_listener_connection{exchange = Exchange} = Connection, + Values = [{<<"Exchange-ID">>, Exchange}], + JObj = kz_json:set_values(Values, Payload), + kapi_sms:publish_outbound(JObj, ?AMQP_PUBLISH_OPTIONS), + #{count := Count, pids := Pids} = Confirms, + Info = #{payload => kz_json:delete_key(<<"Body">>, Payload), from => From}, + {'noreply', State#state{confirms = Confirms#{count => Count + 1, pids => Pids#{Count => Info}}}}; +handle_call(_Request, _From, State) -> + {'reply', {'error', 'not_implemented'}, State}. + +%%------------------------------------------------------------------------------ +%% @doc Handling cast messages. +%% @end +%%------------------------------------------------------------------------------ +-spec handle_cast(any(), state()) -> kz_types:handle_cast_ret_state(state()). +handle_cast({'gen_listener', {'created_queue', _QueueNAme}}, State) -> + {'noreply', State}; +handle_cast({'gen_listener', {'is_consuming', _IsConsuming}}, State) -> + {'noreply', State}; +handle_cast({'gen_listener',{'server_confirms', _Confirms}}, State) -> + {'noreply', State}; +handle_cast({'gen_listener', {'confirm', Confirm}}, State) -> + {'noreply', handle_confirm(Confirm, State)}; +handle_cast(_Msg, State) -> + lager:debug("external listener unhandled cast: ~p", [_Msg]), + {'noreply', State}. + +%%------------------------------------------------------------------------------ +%% @doc Handling all non call/cast messages. +%% @end +%%------------------------------------------------------------------------------ +-spec handle_info(any(), state()) -> kz_types:handle_info_ret_state(state()). +handle_info(_Info, State) -> + lager:debug("external listener unhandled info: ~p", [_Info]), + {'noreply', State}. + +%%------------------------------------------------------------------------------ +%% @doc Allows listener to pass options to handlers. +%% @end +%%------------------------------------------------------------------------------ +-spec handle_event(kz_json:object(), kz_term:proplist()) -> gen_listener:handle_event_return(). +handle_event(_JObj, _State) -> + {'reply', []}. + +%%------------------------------------------------------------------------------ +%% @doc This function is called by a `gen_server' when it is about to +%% terminate. It should be the opposite of `Module:init/1' and do any +%% necessary cleaning up. When it returns, the `gen_server' terminates +%% with Reason. The return value is ignored. +%% +%% @end +%%------------------------------------------------------------------------------ +-spec terminate(any(), state()) -> 'ok'. +terminate(_Reason, _State) -> + lager:debug("external listener terminating : ~s", [_Reason]). + +%%------------------------------------------------------------------------------ +%% @doc Convert process state when code is changed. +%% @end +%%------------------------------------------------------------------------------ +-spec code_change(any(), state(), any()) -> {'ok', state()}. +code_change(_OldVsn, State, _Extra) -> + {'ok', State}. + +%%%============================================================================= +%%% Internal functions +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec handle_inbound(kz_json:object(), pid(), gen_listener:basic_deliver()) -> 'ok'. +handle_inbound(JObj, Srv, Deliver) -> + case maybe_relay_request(JObj) of + 'ack' -> gen_listener:ack(Srv, Deliver); + 'nack' -> gen_listener:ack(Srv, Deliver) + end. + +-spec maybe_relay_request(kz_json:object()) -> 'ack' | 'nack'. +maybe_relay_request(JObj) -> + {Number, Inception} = doodle_util:get_inbound_destination(JObj), + Map = #{number => Number + ,inception => Inception + ,request => JObj + ,route_id => kz_api_sms:route_id(JObj) + ,route_type => kz_api_sms:route_type(JObj) + }, + Routines = [fun lookup_number/1 + ,fun number_has_sms_enabled/1 + ,fun account_from_number/1 + ,fun account_fetch/1 + ,fun reseller_fetch/1 + ,fun account_enabled/1 + ,fun reseller_enabled/1 + ,fun account_has_sms/1 + ,fun reseller_has_sms/1 + ,fun account_standing_is_acceptable/1 + ,fun reseller_standing_is_acceptable/1 + ], + case kz_maps:exec(Routines, Map) of + #{account_id := AccountId, error := Error} -> + lager:warning("external sms request for number ~s validation failed in account ~s : ~p", [Number, AccountId, Error]), + 'nack'; + #{error := Error} -> + lager:warning("validation failed in external sms request for number ~s : ~p", [Number, Error]), + 'nack'; + #{account_id := AccountId, request := Payload} -> + lager:info("accepted external sms request ~s for account ~s", [Number, AccountId]), + API = kz_json:set_value(<<"Account-ID">>, AccountId, Payload), + case kz_im_onnet:route(API) of + 'ok' -> 'ack'; + {'error', _Error} -> + lager:warning("onnet rejected request for account ~s : ~p", [AccountId, _Error]), + 'ack' + end; + M -> + lager:debug("unable to determine account for ~s => ~p", [Number, M]), + 'nack' + end. + +lookup_number(#{account_id := _AccountId} = Map) -> Map; +lookup_number(#{number := Number} = Map) -> + case knm_number:get(Number) of + {'error', _R} -> Map; + {'ok', KNumber} -> Map#{knm => KNumber + ,phone_number => knm_number:phone_number(KNumber) + } + end; +lookup_number(Map) -> Map. + +number_has_sms_enabled(#{knm := PN} = Map) -> + case knm_sms:enabled(PN) of + 'true' -> Map; + 'false' -> maps:without([account_id, account, phone_number] + ,Map#{error => <<"number does not have sms enabled">>} + ) + end; +number_has_sms_enabled(Map) -> Map. + +account_from_number(#{account_id := _AccountId} = Map) -> Map; +account_from_number(#{phone_number := PN} = Map) -> + case knm_phone_number:assigned_to(PN) of + 'undefined' -> Map; + AccountId -> Map#{account_id => AccountId + ,reseller_id => kz_services_reseller:get_id(AccountId) + } + end; +account_from_number(Map) -> Map. + +account_fetch(#{account_id := AccountId} = Map) -> + case kzd_accounts:fetch(AccountId) of + {'error', Error} -> maps:without([account_id], Map#{error => Error}); + {'ok', Account} -> Map#{account => Account} + end; +account_fetch(Map) -> Map. + +reseller_fetch(#{reseller_id := ResellerId} = Map) -> + case kzd_accounts:fetch(ResellerId) of + {'error', Error} -> maps:without([reseller_id], Map#{error => Error}); + {'ok', Reseller} -> Map#{reseller => Reseller} + end; +reseller_fetch(Map) -> Map. + +account_enabled(#{account := Account} = Map) -> + case kzd_accounts:enabled(Account) of + 'true' -> Map; + 'false' -> maps:without([account_id, account] + ,Map#{error => <<"account is disabled">>} + ) + end; +account_enabled(Map) -> Map. + +reseller_enabled(#{reseller := Reseller} = Map) -> + case kzd_accounts:enabled(Reseller) of + 'true' -> Map; + 'false' -> maps:without([account_id, account, reseller_id, reseller] + ,Map#{error => <<"reseller is disabled">>} + ) + end; +reseller_enabled(Map) -> Map. + +account_has_sms(#{account_id := AccountId} = Map) -> + case kz_services_im:is_sms_enabled(AccountId) of + 'true' -> Map; + 'false' -> maps:without([account_id, account] + ,Map#{error => <<"account does not have sms enabled">>} + ) + end; +account_has_sms(Map) -> Map. + +reseller_has_sms(#{reseller_id := ResellerId} = Map) -> + case kz_services_im:is_sms_enabled(ResellerId) of + 'true' -> Map; + 'false' -> maps:without([account_id, account, reseller_id, reseller] + ,Map#{error => <<"reseller does not have sms enabled">>} + ) + end; +reseller_has_sms(Map) -> Map. + +account_standing_is_acceptable(#{account_id := AccountId} = Map) -> + case kz_services_standing:acceptable(AccountId) of + {'true', _} -> Map; + {'false', Error} -> maps:without([account_id, account] + ,Map#{error => maps:get(reason, Error)} + ) + end; +account_standing_is_acceptable(Map) -> Map. + +reseller_standing_is_acceptable(#{reseller_id := ResellerId} = Map) -> + case kz_services_standing:acceptable(ResellerId) of + {'true', _} -> Map; + {'false', Error} -> maps:without([account_id, account, reseller_id, reseller] + ,Map#{error => maps:get(reason, Error)} + ) + end; +reseller_standing_is_acceptable(Map) -> Map. + +handle_confirm(#'basic.ack'{delivery_tag = Idx, multiple = 'true'} + ,#state{confirms = #{pids := Pids} = Confirms} = State + ) -> + Keys = maps:fold(fun reply_ok/3, [], maps:filter(fun(K, _) -> K =< Idx end, Pids)), + State#state{confirms = Confirms#{pids => maps:without(Keys, Pids)}}; +handle_confirm(#'basic.ack'{delivery_tag = Idx, multiple = 'false'} + ,#state{confirms = #{pids := Pids} = Confirms} = State + ) -> + Keys = maps:fold(fun reply_ok/3, [], maps:with([Idx], Pids)), + State#state{confirms = Confirms#{pids => maps:without(Keys, Pids)}}; +handle_confirm(#'basic.nack'{delivery_tag = Idx, multiple = 'true'} + ,#state{confirms = #{pids := Pids} = Confirms} = State + ) -> + Keys = maps:fold(fun reply_error/3, [], maps:filter(fun(K, _) -> K =< Idx end, Pids)), + State#state{confirms = Confirms#{pids => maps:without(Keys, Pids)}}; +handle_confirm(#'basic.nack'{delivery_tag = Idx, multiple = 'false'} + ,#state{confirms = #{pids := Pids} = Confirms} = State + ) -> + Keys = maps:fold(fun reply_error/3, [], maps:with([Idx], Pids)), + State#state{confirms = Confirms#{pids => maps:without(Keys, Pids)}}. + +reply_ok(Key, #{payload := Payload, from := Pid}, Acc) -> + gen_server:reply(Pid, 'ok'), + _ = kz_util:spawn(fun create_ledger/2, ['outbound', Payload]), + [Key | Acc]. + +reply_error(Key, Pid, Acc) -> + gen_server:reply(Pid, {'error', <<"server declined message">>}), + [Key | Acc]. + +-spec create_ledger(kapps_im:direction(), kz_json:object()) -> 'ok'. +create_ledger(Type, Payload) -> + IM = kapps_im:from_payload(Payload), + kapps_im:put_message_id(IM), + lager:debug("creating ~s ledger", [Type]), + kz_im_flat_rate:debit(IM). diff --git a/applications/doodle/src/doodle_inbound_listener_sup.erl b/core/kazoo_im/src/kz_im_offnet_sup.erl similarity index 67% rename from applications/doodle/src/doodle_inbound_listener_sup.erl rename to core/kazoo_im/src/kz_im_offnet_sup.erl index 8b4102ec368..887da236ae3 100644 --- a/applications/doodle/src/doodle_inbound_listener_sup.erl +++ b/core/kazoo_im/src/kz_im_offnet_sup.erl @@ -3,35 +3,40 @@ %%% @doc %%% @end %%%----------------------------------------------------------------------------- --module(doodle_inbound_listener_sup). +-module(kz_im_offnet_sup). -behaviour(supervisor). -export([start_link/0]). + +-export([worker/0]). + -export([init/1]). --export([start_inbound_listener/1]). --include("doodle.hrl"). +-include("kazoo_im.hrl"). -define(SERVER, ?MODULE). --define(CHILDREN, [?WORKER_TYPE('doodle_inbound_listener', 'temporary')]). +-define(CHILDREN, [?WORKER_TYPE('kz_im_offnet', 'temporary')]). %%============================================================================== %% API functions %%============================================================================== %%------------------------------------------------------------------------------ -%% @doc +%% @doc Random Worker. %% @end %%------------------------------------------------------------------------------ --spec start_inbound_listener(amqp_listener_connection()) -> kz_types:startlink_ret(). -start_inbound_listener(Connection) -> - supervisor:start_child(?SERVER, [Connection]). - --spec start_listeners() -> 'ok'. -start_listeners() -> - lists:foreach(fun start_inbound_listener/1, connections()). +-spec worker() -> {'error', 'no_connections'} | pid(). +worker() -> + Listeners = supervisor:which_children(?SERVER), + case length(Listeners) of + 0 -> {'error', 'no_connections'}; + Size -> + Selected = rand:uniform(Size), + {_, Pid, _, _} = lists:nth(Selected, Listeners), + Pid + end. %%------------------------------------------------------------------------------ %% @doc Starts the supervisor. @@ -42,7 +47,7 @@ start_link() -> R = supervisor:start_link({'local', ?SERVER}, ?MODULE, []), case R of {'ok', _} -> start_listeners(); - _Other -> lager:error("error starting inbound_listeners sup : ~p", [_Other]) + _Other -> lager:error("error starting offnet supervisor : ~p", [_Other]) end, R. @@ -65,21 +70,19 @@ init([]) -> SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts}, {'ok', {SupFlags, ?CHILDREN}}. --spec default_connection() -> amqp_listener_connection(). -default_connection() -> - #amqp_listener_connection{name = <<"default">> - ,broker = ?DEFAULT_BROKER - ,exchange = ?DEFAULT_EXCHANGE - ,type = ?DEFAULT_EXCHANGE_TYPE - ,queue = ?DEFAULT_QUEUE_NAME - ,options = [] - }. +%%============================================================================== +%% Private +%%============================================================================== +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ -spec connections() -> amqp_listener_connections(). connections() -> - case kapps_config:get_json(?CONFIG_CAT, <<"connections">>) of - 'undefined' -> [default_connection()]; - JObj -> [default_connection()] ++ kz_json:foldl(fun connections_fold/3, [], JObj) + case kapps_config:get_json(?CONFIG_CAT, [<<"connector">>, <<"connections">>]) of + 'undefined' -> []; + JObj -> kz_json:foldl(fun connections_fold/3, [], JObj) end. -spec connections_fold(kz_json:key(), kz_json:object(), amqp_listener_connections()) -> @@ -90,14 +93,20 @@ connections_fold(K, V, Acc) -> ,exchange = kz_json:get_value(<<"exchange">>, V) ,type = kz_json:get_value(<<"type">>, V) ,queue = kz_json:get_value(<<"queue">>, V) - ,options = connection_options(kz_json:get_json_value(<<"options">>, V)) + ,options = connection_options(kz_json:get_json_value(<<"options">>, V, kz_json:new())) }, [C | Acc]. --spec connection_options(kz_term:api_object()) -> kz_term:proplist(). -connection_options('undefined') -> - connection_options(?DEFAULT_EXCHANGE_OPTIONS_JOBJ); +-spec connection_options(kz_json:object()) -> kz_term:proplist(). connection_options(JObj) -> [{kz_term:to_atom(K, 'true'), V} || {K, V} <- kz_json:to_proplist(JObj) ]. + +-spec start_external_listener(amqp_listener_connection()) -> kz_types:startlink_ret(). +start_external_listener(Connection) -> + supervisor:start_child(?SERVER, [Connection]). + +-spec start_listeners() -> 'ok'. +start_listeners() -> + lists:foreach(fun start_external_listener/1, connections()). diff --git a/core/kazoo_im/src/kz_im_onnet.erl b/core/kazoo_im/src/kz_im_onnet.erl new file mode 100644 index 00000000000..d8eb7ec51f5 --- /dev/null +++ b/core/kazoo_im/src/kz_im_onnet.erl @@ -0,0 +1,395 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2010-2019, 2600Hz +%%% @doc This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(kz_im_onnet). +-behaviour(gen_listener). + +-export([start_link/0]). + +-export([route/1, route/2]). + +%% Responders +-export([handle_outbound/2 + ]). + +-export([init/1 + ,handle_call/3 + ,handle_cast/2 + ,handle_info/2 + ,handle_event/2 + ,terminate/2 + ,code_change/3 + ]). + +-include("kazoo_im.hrl"). + +-define(SERVER, ?MODULE). + +-type state() :: map(). + +-define(BINDINGS, [{'sms', [{'restrict_to', ['outbound']}]}]). + +-define(QUEUE_NAME, <<"im">>). + +-define(QUEUE_OPTIONS, [{'exclusive', 'false'} + ,{'durable', 'true'} + ,{'auto_delete', 'false'} + ,{'arguments', [{<<"x-message-ttl">>, 'infinity'} + ,{<<"x-max-length">>, 'infinity'} + ]} + ]). +-define(CONSUME_OPTIONS, [{'exclusive', 'false'} + ,{'no_ack', 'false'} + ]). + +-define(RESPONDERS, [{{?MODULE, 'handle_outbound'}, [{<<"message">>, <<"outbound">>}]}]). + +-define(AMQP_PUBLISH_OPTIONS, [{'mandatory', 'true'} + ,{'delivery_mode', 2} + ]). + +-define(ROUTE_TIMEOUT, 'infinity'). + +%%%============================================================================= +%%% API +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc Starts the server. +%% @end +%%------------------------------------------------------------------------------ +-spec start_link() -> kz_types:startlink_ret(). +start_link() -> + gen_listener:start_link(?MODULE + ,[{'bindings', ?BINDINGS} + ,{'responders', ?RESPONDERS} + ,{'queue_name', ?QUEUE_NAME} % optional to include + ,{'queue_options', ?QUEUE_OPTIONS} % optional to include + ,{'consume_options', ?CONSUME_OPTIONS} % optional to include + ,{'server_confirms', 'true'} + ] + ,[] + ). + +%%------------------------------------------------------------------------------ +%% @doc Route Internally. +%% @end +%%------------------------------------------------------------------------------ +-spec route(kz_term:api_terms()) -> 'ok' | {'error', 'no_connections'}. +route(Payload) -> + case kz_im_onnet_sup:worker() of + {'error', 'no_connections'} = E -> E; + Pid -> route(Pid, Payload) + end. + +-spec route(pid(), kz_term:api_terms()) -> 'ok'. +route(Pid, Payload) -> + gen_listener:call(Pid, {'route', Payload}, ?ROUTE_TIMEOUT). + +%%%============================================================================= +%%% gen_server callbacks +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc Initializes the server. +%% @end +%%------------------------------------------------------------------------------ +-spec init([]) -> {'ok', state()}. +init([]) -> + {'ok', #{count => 1, pids => #{}}}. + +%%------------------------------------------------------------------------------ +%% @doc Handling call messages. +%% @end +%%------------------------------------------------------------------------------ +-spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_types:handle_call_ret_state(state()). +handle_call({'route', Payload}, From, State) -> + kapi_sms:publish_inbound(Payload, ?AMQP_PUBLISH_OPTIONS), + #{count := Count, pids := Pids} = State, + Info = #{payload => kz_json:delete_key(<<"Body">>, Payload), from => From}, + {'noreply', State#{count => Count + 1, pids => Pids#{Count => Info}}}; +handle_call(_Request, _From, State) -> + {'reply', {'error', 'not_implemented'}, State}. + +%%------------------------------------------------------------------------------ +%% @doc Handling cast messages. +%% @end +%%------------------------------------------------------------------------------ +-spec handle_cast(any(), state()) -> kz_types:handle_cast_ret_state(state()). +handle_cast({'gen_listener', {'created_queue', ?QUEUE_NAME}}, State) -> + {'noreply', State}; +handle_cast({'gen_listener', {'is_consuming', _IsConsuming}}, State) -> + {'noreply', State}; +handle_cast({'gen_listener',{'server_confirms', _Confirms}}, State) -> + {'noreply', State}; +handle_cast({'gen_listener', {'confirm', Confirm}}, State) -> + {'noreply', handle_confirm(Confirm, State)}; +handle_cast(_Msg, State) -> + {'noreply', State}. + +%%------------------------------------------------------------------------------ +%% @doc Handling all non call/cast messages. +%% @end +%%------------------------------------------------------------------------------ +-spec handle_info(any(), state()) -> kz_types:handle_info_ret_state(state()). +handle_info(_Info, State) -> + {'noreply', State}. + +%%------------------------------------------------------------------------------ +%% @doc Allows listener to pass options to handlers. +%% @end +%%------------------------------------------------------------------------------ +-spec handle_event(kz_json:object(), kz_term:proplist()) -> gen_listener:handle_event_return(). +handle_event(_JObj, _State) -> + {'reply', []}. + +%%------------------------------------------------------------------------------ +%% @doc This function is called by a `gen_server' when it is about to +%% terminate. It should be the opposite of `Module:init/1' and do any +%% necessary cleaning up. When it returns, the `gen_server' terminates +%% with Reason. The return value is ignored. +%% +%% @end +%%------------------------------------------------------------------------------ +-spec terminate(any(), state()) -> 'ok'. +terminate(_Reason, _State) -> + lager:debug("listener terminating: ~p", [_Reason]). + +%%------------------------------------------------------------------------------ +%% @doc Convert process state when code is changed. +%% @end +%%------------------------------------------------------------------------------ +-spec code_change(any(), state(), any()) -> {'ok', state()}. +code_change(_OldVsn, State, _Extra) -> + {'ok', State}. + +%%%============================================================================= +%%% Internal functions +%%%============================================================================= + +%%%============================================================================= +%%% Handle Outbound +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec handle_outbound(kz_json:object(), kz_term:proplist()) -> 'ok'. +handle_outbound(JObj, Props) -> + _ = kz_util:put_callid(JObj), + Srv = props:get_value('server', Props), + Deliver = props:get_value('deliver', Props), + case kapi_sms:outbound_v(JObj) + andalso handle_outbound_route(JObj, Props) + of + 'false' -> gen_listener:ack(Srv, Deliver); + 'ack' -> gen_listener:ack(Srv, Deliver); + 'nack' -> gen_listener:ack(Srv, Deliver) + end. + + +-spec handle_outbound_route(kz_json:object(), kz_term:proplist()) -> 'ack' | 'nack'. +handle_outbound_route(JObj, Props) -> + Funs = [fun account/1 + ,fun trusted_application/1 + ,fun account_fetch/1 + ,fun reseller_fetch/1 + ,fun account_is_enabled/1 + ,fun reseller_is_enabled/1 + ,fun account_has_sms/1 + ,fun reseller_has_sms/1 + ,fun account_standing_is_acceptable/1 + ,fun reseller_standing_is_acceptable/1 + ,fun number/1 + ], + route_offnet(kz_maps:exec(Funs + ,#{payload => JObj + ,route => kz_api_sms:route_id(JObj) + ,props => Props + } + )). + +-spec route_offnet(map()) -> 'ack' | 'nack'. +route_offnet(#{enabled := 'true' + ,payload := Payload + ,account_id := AccountId + ,route := RouteId + }) -> + case kz_im_offnet:route(kzd_sms:set_route_id(Payload, RouteId)) of + 'ok' -> 'ack'; + {'error', _Error} -> + lager:warning("offnet rejected request for account ~s : ~p", [AccountId, _Error]), + 'ack' + end; +route_offnet(_Map) -> + lager:debug_unsafe("not routing sms ~p", [_Map]), + 'nack'. + +account(#{payload := JObj} = Map) -> + case kz_api_sms:account_id(JObj) of + 'undefined' -> Map; + AccountId -> Map#{account_id => AccountId + ,reseller_id => kz_services_reseller:get_id(AccountId) + ,enabled => 'true' + } + end. + +trusted_application(#{payload := JObj} = Map) -> + case kz_api_sms:application_id(JObj) of + 'undefined' -> Map; + AppId -> + Trusted = kz_json:get_list_value([<<"outbound">>, <<"trusted_apps">>], config(), []), + case lists:member(AppId, Trusted) of + 'false' -> Map; + 'true' -> Map#{enabled => 'true'} + end + end. + +account_fetch(#{account_id := AccountId} = Map) -> + case kzd_accounts:fetch(AccountId) of + {'error', Error} -> maps:without([account_id], Map#{error => Error}); + {'ok', Account} -> Map#{account => Account} + end; +account_fetch(Map) -> Map. + +reseller_fetch(#{reseller_id := ResellerId} = Map) -> + case kzd_accounts:fetch(ResellerId) of + {'error', Error} -> maps:without([reseller_id], Map#{error => Error}); + {'ok', Reseller} -> Map#{reseller => Reseller} + end; +reseller_fetch(Map) -> Map. + +account_is_enabled(#{account := Account} = Map) -> + case kzd_accounts:enabled(Account) of + 'true' -> Map; + 'false' -> Map#{enabled => 'false'} + end; +account_is_enabled(Map) -> Map. + + +reseller_is_enabled(#{reseller := Reseller} = Map) -> + case kzd_accounts:enabled(Reseller) of + 'true' -> Map; + 'false' -> Map#{enabled => 'false'} + end; +reseller_is_enabled(Map) -> Map. + +account_has_sms(#{account_id := AccountId} = Map) -> + case kz_services_im:is_sms_enabled(AccountId) of + 'true' -> Map; + 'false' -> Map#{enabled => 'false'} + end; +account_has_sms(Map) -> Map. + +reseller_has_sms(#{reseller_id := ResellerId} = Map) -> + case kz_services_im:is_sms_enabled(ResellerId) of + 'true' -> Map; + 'false' -> Map#{enabled => 'false'} + end; +reseller_has_sms(Map) -> Map. + +-define(STANDING_ACCEPTABLE_OPTIONS, #{cache_acceptable => true}). + +account_standing_is_acceptable(#{account_id := AccountId} = Map) -> + case kz_services_standing:acceptable(AccountId, ?STANDING_ACCEPTABLE_OPTIONS) of + {'true', _} -> Map; + _Else -> + lager:debug("ACCOUNT STANDING => ~p", [_Else]), + Map#{enabled => 'false'} + end; +account_standing_is_acceptable(Map) -> Map. + +reseller_standing_is_acceptable(#{reseller_id := ResellerId} = Map) -> + case kz_services_standing:acceptable(ResellerId, ?STANDING_ACCEPTABLE_OPTIONS) of + {'true', _} -> Map; + _Else -> + lager:debug("RESELLER STANDING => ~p", [_Else]), + Map#{enabled => 'false'} + end; +reseller_standing_is_acceptable(Map) -> Map. + +number(#{enabled := 'false'} = Map) -> Map; +number(#{payload := JObj} = Map) -> + case knm_number:get(kz_api_sms:from(JObj)) of + {'ok', Num} -> + case knm_sms:enabled(Num) + andalso number_provider(Num) + of + 'false' -> + lager:debug("number does not have sms enabled"), + Map#{enabled => 'false'}; + 'undefined' -> + Map#{number => Num}; + Provider -> + Setters = [{fun kz_api_sms:set_originator_property/3, <<"Number-Provider">>, Provider} + ,{fun kz_api_sms:set_originator_flag/2, Provider} + ], + Map#{number => Num + ,module => Provider + ,route => Provider + ,payload => kz_json:exec_first(Setters, JObj) + } + end; + _ -> Map + end. + +number_provider(Num) -> + Mod = knm_phone_number:module_name(knm_number:phone_number(Num)), + kz_json:get_ne_binary_value([<<"outbound">>, <<"knm">>, Mod], config(), Mod). + +config() -> + case kapps_config:get_category(?APP_NAME) of + {'ok', JObj} -> kz_json:get_json_value([<<"default">>, <<"connector">>], JObj, kz_json:new()); + _ -> kz_json:new() + end. + +%%%============================================================================= +%%% Handle Confirms +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +handle_confirm(#'basic.ack'{delivery_tag = Idx, multiple = 'true'} + ,#{pids := Pids} = State + ) -> + Keys = maps:fold(fun reply_ok/3, [], maps:filter(fun(K, _) -> K =< Idx end, Pids)), + State#{pids => maps:without(Keys, Pids)}; +handle_confirm(#'basic.ack'{delivery_tag = Idx, multiple = 'false'} + ,#{pids := Pids} = State + ) -> + Keys = maps:fold(fun reply_ok/3, [], maps:with([Idx], Pids)), + State#{pids => maps:without(Keys, Pids)}; +handle_confirm(#'basic.nack'{delivery_tag = Idx, multiple = 'true'} + ,#{pids := Pids} = State + ) -> + Keys = maps:fold(fun reply_error/3, [], maps:filter(fun(K, _) -> K =< Idx end, Pids)), + State#{pids => maps:without(Keys, Pids)}; +handle_confirm(#'basic.nack'{delivery_tag = Idx, multiple = 'false'} + ,#{pids := Pids} = State + ) -> + Keys = maps:fold(fun reply_error/3, [], maps:with([Idx], Pids)), + State#{pids => maps:without(Keys, Pids)}. + +reply_ok(Key, #{payload := Payload, from := Pid}, Acc) -> + gen_server:reply(Pid, 'ok'), + _ = kz_util:spawn(fun create_ledger/2, ['inbound', Payload]), + [Key | Acc]. + +reply_error(Key, Pid, Acc) -> + gen_server:reply(Pid, {'error', <<"server declined message">>}), + [Key | Acc]. + +-spec create_ledger(kapps_im:direction(), kz_json:object()) -> 'ok'. +create_ledger(Type, Payload) -> + IM = kapps_im:from_payload(Payload), + kapps_im:put_message_id(IM), + lager:debug("creating ~s ledger", [Type]), + kz_im_flat_rate:debit(IM). diff --git a/core/kazoo_im/src/kz_im_onnet_sup.erl b/core/kazoo_im/src/kz_im_onnet_sup.erl new file mode 100644 index 00000000000..3ee847554eb --- /dev/null +++ b/core/kazoo_im/src/kz_im_onnet_sup.erl @@ -0,0 +1,81 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2012-2019, 2600Hz +%%% @doc This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(kz_im_onnet_sup). + +-behaviour(supervisor). + +-include("kazoo_im.hrl"). + +-define(SERVER, ?MODULE). + +-export([start_link/0]). + +-export([worker/0]). + +-export([init/1]). + +-define(CHILDREN, [?WORKER_ARGS_TYPE('kz_im_onnet', [], 'temporary')]). + +%% =================================================================== +%% API functions +%% =================================================================== + +%%------------------------------------------------------------------------------ +%% @doc Starts the supervisor +%% @end +%%------------------------------------------------------------------------------ +-spec start_link() -> kz_types:startlink_ret(). +start_link() -> + {'ok', Pid} = supervisor:start_link({'local', ?SERVER}, ?MODULE, []), + _ = init_workers(Pid), + {'ok', Pid}. + +%%------------------------------------------------------------------------------ +%% @doc Random Worker. +%% @end +%%------------------------------------------------------------------------------ +-spec worker() -> {'error', 'no_connections'} | pid(). +worker() -> + Listeners = supervisor:which_children(?SERVER), + case length(Listeners) of + 0 -> {'error', 'no_connections'}; + Size -> + Selected = rand:uniform(Size), + {_, Pid, _, _} = lists:nth(Selected, Listeners), + Pid + end. + +%% =================================================================== +%% Supervisor callbacks +%% =================================================================== + +%%------------------------------------------------------------------------------ +%% @doc Whenever a supervisor is started using supervisor:start_link/[2,3], +%% this function is called by the new process to find out about +%% restart strategy, maximum restart frequency and child +%% specifications. +%% @end +%%------------------------------------------------------------------------------ +-spec init(list()) -> kz_types:sup_init_ret(). +init([]) -> + RestartStrategy = 'simple_one_for_one', + MaxRestarts = 0, + MaxSecondsBetweenRestarts = 1, + SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts}, + {'ok', {SupFlags, ?CHILDREN}}. + +init_workers(Pid) -> + Workers = kapps_config:get_integer(?CONFIG_CAT, <<"onnet_listeners">>, 1), + _ = kz_util:spawn(fun() -> [begin + _ = supervisor:start_child(Pid, []), + timer:sleep(500) + end + || _N <- lists:seq(1, Workers) + ] + end). diff --git a/core/kazoo_number_manager/include/knm_phone_number.hrl b/core/kazoo_number_manager/include/knm_phone_number.hrl index 90fbbfadbf7..cf3222753d7 100644 --- a/core/kazoo_number_manager/include/knm_phone_number.hrl +++ b/core/kazoo_number_manager/include/knm_phone_number.hrl @@ -80,6 +80,8 @@ -define(FEATURE_PREPEND, <<"prepend">>). -define(FEATURE_RENAME_CARRIER, <<"carrier_name">>). -define(FEATURE_RINGBACK, <<"ringback">>). +-define(FEATURE_SMS, <<"sms">>). +-define(FEATURE_MMS, <<"mms">>). -define(PROVIDER_RENAME_CARRIER, <<"knm_rename_carrier">>). -define(PROVIDER_FORCE_OUTBOUND, <<"knm_", (?FEATURE_FORCE_OUTBOUND)/binary>>). @@ -99,6 +101,8 @@ ,?FEATURE_CNAM_OUTBOUND ,?FEATURE_E911 ,?FEATURE_PORT + ,?FEATURE_SMS + ,?FEATURE_MMS ]). -define(ADMIN_ONLY_FEATURES, [?FEATURE_RENAME_CARRIER @@ -116,6 +120,8 @@ ,?FEATURE_PREPEND ,?FEATURE_RENAME_CARRIER ,?FEATURE_RINGBACK + ,?FEATURE_SMS + ,?FEATURE_MMS ]). -define(CNAM_DISPLAY_NAME, <<"display_name">>). @@ -139,5 +145,7 @@ -define(FAILOVER_E164, <<"e164">>). -define(FAILOVER_SIP, <<"sip">>). +-define(FEATURE_ENABLED, <<"enabled">>). + -define(KNM_NUMBER_MANAGER_HRL, 'true'). -endif. diff --git a/core/kazoo_number_manager/src/knm_number.erl b/core/kazoo_number_manager/src/knm_number.erl index d2c7743f4fe..9eeddd69367 100644 --- a/core/kazoo_number_manager/src/knm_number.erl +++ b/core/kazoo_number_manager/src/knm_number.erl @@ -425,6 +425,8 @@ check_account(PN) -> ,{'ringback_media', find_early_ringback(PN)} ,{'transfer_media', find_transfer_ringback(PN)} ,{'force_outbound', is_force_outbound(PN)} + ,{'sms', feature_sms(PN)} + ,{'mms', feature_mms(PN)} ], {'ok', AssignedTo, Props} end. @@ -467,6 +469,30 @@ feature_prepend(PhoneNumber) -> 'true' -> kz_json:get_ne_value(?PREPEND_NAME, Prepend) end. +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec feature_sms(knm_phone_number:knm_phone_number()) -> kz_term:api_binary(). +feature_sms(PhoneNumber) -> + Sms = knm_phone_number:feature(PhoneNumber, ?FEATURE_SMS), + case kz_json:is_true(?FEATURE_ENABLED, Sms) of + 'false' -> 'undefined'; + 'true' -> Sms + end. + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec feature_mms(knm_phone_number:knm_phone_number()) -> kz_term:api_binary(). +feature_mms(PhoneNumber) -> + Mms = knm_phone_number:feature(PhoneNumber, ?FEATURE_MMS), + case kz_json:is_true(?FEATURE_ENABLED, Mms) of + 'false' -> 'undefined'; + 'true' -> Mms + end. + %%------------------------------------------------------------------------------ %% @doc %% @end diff --git a/core/kazoo_number_manager/src/knm_phone_number.erl b/core/kazoo_number_manager/src/knm_phone_number.erl index 58cdd795b78..c0733e95adb 100644 --- a/core/kazoo_number_manager/src/knm_phone_number.erl +++ b/core/kazoo_number_manager/src/knm_phone_number.erl @@ -428,9 +428,7 @@ fetch(Num=?NE_BINARY, Options) -> NumberDb = knm_converters:to_db(NormalizedNum), case fetch(NumberDb, NormalizedNum, Options) of {'ok', JObj} -> handle_fetch(JObj, Options); - {'error', _R}=Error -> - lager:debug("failed to open ~s in ~s: ~p", [NormalizedNum, NumberDb, _R]), - Error + {'error', _R}=Error -> Error end. -spec fetch(kz_term:ne_binary(), kz_term:ne_binary(), knm_number_options:options()) -> @@ -1574,6 +1572,8 @@ private_to_public() -> ,?FEATURE_FAILOVER => FailoverPub ,?FEATURE_RINGBACK => RingbackPub ,?FEATURE_FORCE_OUTBOUND => [[?FEATURE_FORCE_OUTBOUND]] + ,?FEATURE_SMS => [[?FEATURE_SMS]] + ,?FEATURE_MMS => [[?FEATURE_MMS]] }. %%------------------------------------------------------------------------------ @@ -1610,9 +1610,7 @@ sanitize_public_fields(JObj) -> knm_numbers:collection() | boolean(). is_authorized(T) when is_map(T) -> is_authorized_collection(T); -is_authorized(#knm_phone_number{auth_by = ?KNM_DEFAULT_AUTH_BY}) -> - lager:info("bypassing auth"), - 'true'; +is_authorized(#knm_phone_number{auth_by = ?KNM_DEFAULT_AUTH_BY}) -> 'true'; is_authorized(#knm_phone_number{auth_by = 'undefined'}) -> 'false'; is_authorized(#knm_phone_number{assigned_to = 'undefined' ,assign_to = 'undefined' diff --git a/core/kazoo_number_manager/src/providers/knm_mms.erl b/core/kazoo_number_manager/src/providers/knm_mms.erl new file mode 100644 index 00000000000..287b0dbed88 --- /dev/null +++ b/core/kazoo_number_manager/src/providers/knm_mms.erl @@ -0,0 +1,93 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2010-2019, 2600Hz +%%% @doc Handle prepend feature +%%% @author Peter Defebvre +%%% This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(knm_mms). +-behaviour(knm_gen_provider). + +-export([save/1]). +-export([delete/1]). +-export([enabled/1]). + +-include("knm.hrl"). + +-define(KEY, ?FEATURE_MMS). + +%%------------------------------------------------------------------------------ +%% @doc This function is called each time a number is saved, and will +%% add the prepend route (for in service numbers only) +%% @end +%%------------------------------------------------------------------------------ + +-spec save(knm_number:knm_number()) -> knm_number:knm_number(). +save(PN) -> + State = knm_phone_number:state(knm_number:phone_number(PN)), + save(PN, State). + +-spec save(knm_number:knm_number(), kz_term:ne_binary()) -> knm_number:knm_number(). +save(PN, ?NUMBER_STATE_IN_SERVICE) -> + update_mms(PN); +save(PN, _State) -> + delete(PN). + +%%------------------------------------------------------------------------------ +%% @doc This function is called each time a number is deleted, and will +%% remove the prepend route +%% @end +%%------------------------------------------------------------------------------ +-spec delete(knm_number:knm_number()) -> knm_number:knm_number(). +delete(PN) -> + case feature(PN) of + 'undefined' -> PN; + _Else -> knm_providers:deactivate_feature(PN, ?KEY) + end. + +-spec enabled(knm_number:knm_number()) -> boolean(). +enabled(PN) -> + Feature = feature(PN), + case kz_term:is_empty(Feature) of + 'true' -> 'false'; + 'false' -> kz_json:is_true(?FEATURE_ENABLED, Feature) + end. + +%%%============================================================================= +%%% Internal functions +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec update_mms(knm_number:knm_number()) -> knm_number:knm_number(). +update_mms(PN) -> + CurrentFeature = feature(PN), + PhoneNumber = knm_number:phone_number(PN), + Feature = kz_json:get_ne_value(?KEY, knm_phone_number:doc(PhoneNumber)), + NotChanged = kz_json:are_equal(CurrentFeature, Feature), + case kz_term:is_empty(Feature) of + 'true' -> + knm_providers:deactivate_feature(PN, ?KEY); + 'false' when NotChanged -> + PN; + 'false' -> + case kz_json:is_true(?FEATURE_ENABLED, Feature) of + 'false' -> knm_providers:deactivate_feature(PN, ?KEY); + 'true' -> knm_providers:activate_feature(PN, {?KEY, Feature}) + end + end. + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec feature(knm_number:knm_number()) -> kz_json:api_json_term(). +feature(Number) -> + knm_phone_number:feature(knm_number:phone_number(Number), ?KEY). + + diff --git a/core/kazoo_number_manager/src/providers/knm_providers.erl b/core/kazoo_number_manager/src/providers/knm_providers.erl index a535863f292..eb0d971a143 100644 --- a/core/kazoo_number_manager/src/providers/knm_providers.erl +++ b/core/kazoo_number_manager/src/providers/knm_providers.erl @@ -394,6 +394,10 @@ provider_module(?FEATURE_E911, ?MATCH_ACCOUNT_RAW(AccountId)) -> e911_provider(AccountId); provider_module(?FEATURE_PREPEND, _) -> <<"knm_prepend">>; +provider_module(?FEATURE_SMS, _) -> + <<"knm_sms">>; +provider_module(?FEATURE_MMS, _) -> + <<"knm_mms">>; provider_module(?FEATURE_PORT, _) -> <<"knm_port_notifier">>; provider_module(?FEATURE_FAILOVER, _) -> diff --git a/core/kazoo_number_manager/src/providers/knm_sms.erl b/core/kazoo_number_manager/src/providers/knm_sms.erl new file mode 100644 index 00000000000..243370402ef --- /dev/null +++ b/core/kazoo_number_manager/src/providers/knm_sms.erl @@ -0,0 +1,92 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2010-2019, 2600Hz +%%% @doc Handle prepend feature +%%% @author Peter Defebvre +%%% This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(knm_sms). +-behaviour(knm_gen_provider). + +-export([save/1]). +-export([delete/1]). +-export([enabled/1]). + +-include("knm.hrl"). + +-define(KEY, ?FEATURE_SMS). + +%%------------------------------------------------------------------------------ +%% @doc This function is called each time a number is saved, and will +%% add the prepend route (for in service numbers only) +%% @end +%%------------------------------------------------------------------------------ + +-spec save(knm_number:knm_number()) -> knm_number:knm_number(). +save(PN) -> + State = knm_phone_number:state(knm_number:phone_number(PN)), + save(PN, State). + +-spec save(knm_number:knm_number(), kz_term:ne_binary()) -> knm_number:knm_number(). +save(PN, ?NUMBER_STATE_IN_SERVICE) -> + update_sms(PN); +save(PN, _State) -> + delete(PN). + +%%------------------------------------------------------------------------------ +%% @doc This function is called each time a number is deleted, and will +%% remove the prepend route +%% @end +%%------------------------------------------------------------------------------ +-spec delete(knm_number:knm_number()) -> knm_number:knm_number(). +delete(Number) -> + case feature(Number) of + 'undefined' -> Number; + _Else -> knm_providers:deactivate_feature(Number, ?KEY) + end. + +-spec enabled(knm_number:knm_number()) -> boolean(). +enabled(PN) -> + Feature = feature(PN), + case kz_term:is_empty(Feature) of + 'true' -> 'false'; + 'false' -> kz_json:is_true(?FEATURE_ENABLED, Feature) + end. + +%%%============================================================================= +%%% Internal functions +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec update_sms(knm_number:knm_number()) -> knm_number:knm_number(). +update_sms(PN) -> + CurrentFeature = feature(PN), + PhoneNumber = knm_number:phone_number(PN), + Feature = kz_json:get_ne_value(?KEY, knm_phone_number:doc(PhoneNumber)), + NotChanged = kz_json:are_equal(CurrentFeature, Feature), + case kz_term:is_empty(Feature) of + 'true' -> + knm_providers:deactivate_feature(PN, ?KEY); + 'false' when NotChanged -> + PN; + 'false' -> + case kz_json:is_true(?FEATURE_ENABLED, Feature) of + 'false' -> knm_providers:deactivate_feature(PN, ?KEY); + 'true' -> knm_providers:activate_feature(PN, {?KEY, Feature}) + end + end. + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec feature(knm_number:knm_number()) -> kz_json:api_json_term(). +feature(Number) -> + knm_phone_number:feature(knm_number:phone_number(Number), ?KEY). + diff --git a/core/kazoo_services/src/kz_services_plan.erl b/core/kazoo_services/src/kz_services_plan.erl index 463c77a70b4..0fb55671249 100644 --- a/core/kazoo_services/src/kz_services_plan.erl +++ b/core/kazoo_services/src/kz_services_plan.erl @@ -21,6 +21,7 @@ -export([ratedeck_name/1]). -export([applications/1]). -export([asr/1]). +-export([im/1]). -export([limits/1]). -export([jobj/1 ,set_jobj/2 @@ -178,6 +179,14 @@ applications(Plan) -> asr(Plan) -> kzd_service_plan:asr(jobj(Plan)). +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec im(plan()) -> kz_json:object(). +im(Plan) -> + kzd_service_plan:im(jobj(Plan)). + %%------------------------------------------------------------------------------ %% @doc %% @end diff --git a/core/kazoo_services/src/modules/kz_services_im.erl b/core/kazoo_services/src/modules/kz_services_im.erl new file mode 100644 index 00000000000..3b6ce3a39d0 --- /dev/null +++ b/core/kazoo_services/src/modules/kz_services_im.erl @@ -0,0 +1,92 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2012-2019, 2600Hz +%%% @doc This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(kz_services_im). + +-export([fetch/1 + ,flat_rate/3 + ,is_sms_enabled/1 + ,is_mms_enabled/1 + ]). + +-include("services.hrl"). + +-define(DEFAULT_SMS_INBOUND_FLAT_RATE, 0.007). +-define(DEFAULT_SMS_OUTBOUND_FLAT_RATE, 0.007). +-define(DEFAULT_MMS_INBOUND_FLAT_RATE, 0.015). +-define(DEFAULT_MMS_OUTBOUND_FLAT_RATE, 0.015). + +-type direction() :: kapps_im:direction(). +-type im_type() :: kapps_im:im_type(). + +-export_type([direction/0 + ,im_type/0 + ]). + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec fetch(kz_services:services() | kz_term:ne_binary()) -> kz_json:object(). +fetch(?NE_BINARY=AccountId) -> + FetchOptions = ['hydrate_plans'], + fetch(kz_services:fetch(AccountId, FetchOptions)); +fetch(Services) -> + IMDict = kz_services_plans:foldl(fun fetch_foldl/3 + ,dict:new() + ,kz_services:plans(Services) + ), + kz_json:from_list(dict:to_list(IMDict)). + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec fetch_foldl(kz_term:ne_binary(), kz_services_plans:plans_list(), dict:dict()) -> dict:dict(). +fetch_foldl(_BookkeeperHash, [], Providers) -> + Providers; +fetch_foldl(_BookkeeperHash, PlansList, Providers) -> + Plan = kz_services_plans:merge(PlansList), + kz_json:foldl(fun(K, V, A) -> + dict:store(K, V, A) + end + ,Providers + ,kz_services_plan:im(Plan) + ). + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec flat_rate(kz_term:ne_binary(), im_type(), direction()) -> kz_currency:dollars(). +flat_rate(AccountId, IM, Direction) -> + Default = default_flat_rate(IM, Direction), + Items = fetch(AccountId), + Key = [kz_term:to_binary(IM) + ,<<"rate">> + ,kz_term:to_binary(Direction) + ], + kz_json:get_number_value(Key, Items, Default). + +default_flat_rate('sms', 'inbound') -> ?DEFAULT_SMS_INBOUND_FLAT_RATE; +default_flat_rate('sms', 'outbound') -> ?DEFAULT_SMS_OUTBOUND_FLAT_RATE; +default_flat_rate('mms', 'inbound') -> ?DEFAULT_MMS_INBOUND_FLAT_RATE; +default_flat_rate('mms', 'outbound') -> ?DEFAULT_MMS_OUTBOUND_FLAT_RATE; +default_flat_rate(_, _) -> 0. + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec is_sms_enabled(kz_services:services() | kz_term:ne_binary()) -> boolean(). +is_sms_enabled(AccountId) -> + kz_json:is_true([<<"sms">>, <<"enabled">>], fetch(AccountId)). + +-spec is_mms_enabled(kz_services:services() | kz_term:ne_binary()) -> boolean(). +is_mms_enabled(AccountId) -> + kz_json:is_true([<<"mms">>, <<"enabled">>], fetch(AccountId)).