From 0e7e374344bd67b79068818e6011d5b0e980f9f1 Mon Sep 17 00:00:00 2001 From: John White Date: Wed, 1 Apr 2020 09:11:53 -0500 Subject: [PATCH] KZOO-46: Add Emergency Notification for E911 Compliance (#6443) This PR adds new functionality to help facilitate compliance with Kari's Law and notification for when an emergency call is placed. An email contact list can now be set on a phone numbers emergency metadata. Notification can now be achieved one of several ways: * A new amqp event, `emergency_bridge` has been added to `kapi_notifications` to trigger a teletype notification email when an emergency call is placed. * A new dedicated emergency bridge webhook can be configured * Adds new text and html teletype templates * Adds new teletype module to render template * Template will distinguish between Emergency and test calls * Creates a new amqp definition ing `kapi_notifications` for usage when an emergency call is attempted. * Publish callback as been added to `cb_notifications` * Set emergency location data as well as user and device metadata * Publishes `emergency_bridge` message when stepswitch determines an emergency call is being attempted * Adds a new test rules object a resource to define test patterns for a resource to help differentiate between emergency and test calls. --- applications/crossbar/doc/phone_numbers.md | 2 + .../crossbar/doc/ref/phone_numbers.md | 2 + applications/crossbar/doc/ref/resources.md | 2 + applications/crossbar/doc/resources.md | 18 +++ applications/crossbar/priv/api/swagger.json | 149 ++++++++++++++++++ .../kapi.notifications.emergency_bridge.json | 135 ++++++++++++++++ .../priv/couchdb/schemas/phone_numbers.json | 8 + .../priv/couchdb/schemas/resources.json | 8 + .../crossbar/priv/oas3/oas3-schemas.yml | 13 ++ .../crossbar/src/modules/cb_notifications.erl | 2 + applications/stepswitch/doc/emergency.md | 25 +++ applications/stepswitch/doc/rules.md | 9 ++ .../stepswitch/src/stepswitch_bridge.erl | 111 ++++++++++++- .../stepswitch/src/stepswitch_resources.erl | 48 +++++- applications/teletype/doc/README.md | 16 ++ .../priv/templates/emergency_bridge.html | 115 ++++++++++++++ .../priv/templates/emergency_bridge.text | 46 ++++++ .../templates/teletype_emergency_bridge.erl | 126 +++++++++++++++ .../teletype/test/teletype_render_tests.erl | 3 +- core/kazoo_amqp/src/api/kapi_dialplan.erl | 19 +-- .../kazoo_amqp/src/api/kapi_notifications.erl | 79 ++++++++++ core/kazoo_amqp/src/api/kz_api.erl | 10 +- core/kazoo_apps/src/kz_amqp_worker.erl | 4 +- core/kazoo_call/src/kz_call_response.erl | 12 +- .../kazoo_documents/src/kzd_phone_numbers.erl | 13 ++ .../src/kzd_phone_numbers.erl.src | 13 ++ core/kazoo_documents/src/kzd_resources.erl | 13 ++ .../kazoo_documents/src/kzd_resources.erl.src | 13 ++ core/kazoo_stdlib/src/props.erl | 6 +- doc/mkdocs/mkdocs.yml | 1 + 30 files changed, 992 insertions(+), 29 deletions(-) create mode 100644 applications/crossbar/priv/couchdb/schemas/kapi.notifications.emergency_bridge.json create mode 100644 applications/stepswitch/doc/emergency.md create mode 100644 applications/teletype/priv/templates/emergency_bridge.html create mode 100644 applications/teletype/priv/templates/emergency_bridge.text create mode 100644 applications/teletype/src/templates/teletype_emergency_bridge.erl diff --git a/applications/crossbar/doc/phone_numbers.md b/applications/crossbar/doc/phone_numbers.md index c583f49f0c0..83cdef6d2f0 100644 --- a/applications/crossbar/doc/phone_numbers.md +++ b/applications/crossbar/doc/phone_numbers.md @@ -39,6 +39,8 @@ Key | Description | Type | Default | Required | Support Level `e911.locality` | The locality (city) where the number is in service | `string()` | | `true` | `e911.location_id` | The e911 provisioning system internal id for this service address | `string()` | | `false` | `e911.longitude` | The e911 provisioning system calculated service address longitude | `string()` | | `false` | +`e911.notification_contact_emails.[]` | | `string()` | | `false` | +`e911.notification_contact_emails` | A list of email addresses to receive notification when this number places an emergency call | `array(string())` | `[]` | `false` | `e911.plus_four` | The extended zip/postal code where the number is in service | `string()` | | `false` | `e911.postal_code` | The zip/postal code where the number is in service | `string()` | | `true` | `e911.region` | The region (state) where the number is in service | `string(2)` | | `true` | diff --git a/applications/crossbar/doc/ref/phone_numbers.md b/applications/crossbar/doc/ref/phone_numbers.md index 1bc932ecba4..289bfab92eb 100644 --- a/applications/crossbar/doc/ref/phone_numbers.md +++ b/applications/crossbar/doc/ref/phone_numbers.md @@ -27,6 +27,8 @@ Key | Description | Type | Default | Required | Support Level `e911.locality` | The locality (city) where the number is in service | `string()` | | `true` | `e911.location_id` | The e911 provisioning system internal id for this service address | `string()` | | `false` | `e911.longitude` | The e911 provisioning system calculated service address longitude | `string()` | | `false` | +`e911.notification_contact_emails.[]` | | `string()` | | `false` | +`e911.notification_contact_emails` | A list of email addresses to receive notification when this number places an emergency call | `array(string())` | `[]` | `false` | `e911.plus_four` | The extended zip/postal code where the number is in service | `string()` | | `false` | `e911.postal_code` | The zip/postal code where the number is in service | `string()` | | `true` | `e911.region` | The region (state) where the number is in service | `string(2)` | | `true` | diff --git a/applications/crossbar/doc/ref/resources.md b/applications/crossbar/doc/ref/resources.md index 7329f7ad448..910d336b893 100644 --- a/applications/crossbar/doc/ref/resources.md +++ b/applications/crossbar/doc/ref/resources.md @@ -77,6 +77,8 @@ Key | Description | Type | Default | Required | Support Level `require_flags` | When set to true this resource is ignored if the request does not specify outbound flags | `boolean()` | | `false` | `rules.[]` | | `string()` | | `false` | `rules` | A list of regular expressions of which one must match for the rule to be eligible, they can optionally contain capture groups | `array(string())` | `[]` | `false` | +`rules_test.[]` | | `string()` | | `false` | +`rules_test` | A list of regular expressions of which if matched denotes a test rule | `array(string())` | `[]` | `false` | `weight_cost` | A value between 0 and 100 that determines the order of resources when multiple can be used | `integer()` | `50` | `false` | ### custom_sip_headers diff --git a/applications/crossbar/doc/resources.md b/applications/crossbar/doc/resources.md index 760483a1f68..e14254d4053 100644 --- a/applications/crossbar/doc/resources.md +++ b/applications/crossbar/doc/resources.md @@ -93,6 +93,8 @@ Key | Description | Type | Default | Required | Support Level `require_flags` | When set to true this resource is ignored if the request does not specify outbound flags | `boolean()` | | `false` | `rules.[]` | | `string()` | | `false` | `rules` | A list of regular expressions of which one must match for the rule to be eligible, they can optionally contain capture groups | `array(string())` | `[]` | `false` | +`rules_test.[]` | | `string()` | | `false` | +`rules_test` | A list of regular expressions of which if matched denotes a test rule | `array(string())` | `[]` | `false` | `weight_cost` | A value between 0 and 100 that determines the order of resources when multiple can be used | `integer()` | `50` | `false` | ### custom_sip_headers @@ -191,6 +193,22 @@ Some upstream carriers require the From address' realm to be formatted. There ar 2. Set `"from_account_realm":true` to use the calling account's realm 3. Set `"realm":"{CUSTOM_REALM}"` on a per-gateway basis (not on the top-level resource) + +## rules_test.[] + +The `rules_test` object defines an array of regular expressions for test patterns of the given resource. + +For example, if the resource handles emergency routes in North America: + +``` + "rules_test": [ + "^\\+{0,1}(933)$" + ], +``` + +defining `933` as a test route, will inform teletype this emergency call is a test and will be reflected as such in the notification. + + ## Fetch > GET /v2/accounts/{ACCOUNT_ID}/resources diff --git a/applications/crossbar/priv/api/swagger.json b/applications/crossbar/priv/api/swagger.json index 4cab3385837..8f9272134a0 100644 --- a/applications/crossbar/priv/api/swagger.json +++ b/applications/crossbar/priv/api/swagger.json @@ -19857,6 +19857,139 @@ ], "type": "object" }, + "kapi.notifications.emergency_bridge": { + "description": "AMQP API for notifications.emergency_bridge", + "properties": { + "Account-DB": { + "type": "string" + }, + "Account-ID": { + "type": "string" + }, + "Account-Name": { + "type": "string" + }, + "Attachment-URL": { + "type": "string" + }, + "Authorizing-ID": { + "type": "string" + }, + "Bcc": { + "type": "string" + }, + "Call-ID": { + "type": "string" + }, + "Cc": { + "type": "string" + }, + "Device-ID": { + "type": "string" + }, + "Device-Name": { + "type": "string" + }, + "Device-Owner-ID": { + "type": "string" + }, + "Emergency-Address-City": { + "type": "string" + }, + "Emergency-Address-Latitude": { + "type": "string" + }, + "Emergency-Address-Longitude": { + "type": "string" + }, + "Emergency-Address-Postal-Code": { + "type": "string" + }, + "Emergency-Address-Region": { + "type": "string" + }, + "Emergency-Address-Street-1": { + "type": "string" + }, + "Emergency-Address-Street-2": { + "type": "string" + }, + "Emergency-Caller-ID-Name": { + "type": "string" + }, + "Emergency-Caller-ID-Number": { + "type": "string" + }, + "Emergency-Notfication-Contact-Emails": { + "type": "string" + }, + "Emergency-Test-Call": { + "type": "string" + }, + "Emergency-To-DID": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "notification" + ], + "type": "string" + }, + "Event-Name": { + "enum": [ + "emergency_bridge" + ], + "type": "string" + }, + "From": { + "type": "string" + }, + "HTML": { + "type": "string" + }, + "Outbound-Caller-ID-Name": { + "type": "string" + }, + "Outbound-Caller-ID-Number": { + "type": "string" + }, + "Owner-ID": { + "type": "string" + }, + "Preview": { + "type": "boolean" + }, + "Realm": { + "type": "string" + }, + "Reply-To": { + "type": "string" + }, + "Subject": { + "type": "string" + }, + "Text": { + "type": "string" + }, + "To": { + "type": "string" + }, + "User-Email": { + "type": "string" + }, + "User-First-Name": { + "type": "string" + }, + "User-Last-Name": { + "type": "string" + } + }, + "required": [ + "Account-ID", + "Call-ID" + ], + "type": "object" + }, "kapi.notifications.first_occurrence": { "description": "AMQP API for notifications.first_occurrence", "properties": { @@ -30212,6 +30345,14 @@ "description": "The e911 provisioning system calculated service address longitude", "type": "string" }, + "notification_contact_emails": { + "default": [], + "description": "A list of email addresses to receive notification when this number places an emergency call", + "items": { + "type": "string" + }, + "type": "array" + }, "plus_four": { "description": "The extended zip/postal code where the number is in service", "type": "string" @@ -31868,6 +32009,14 @@ }, "type": "array" }, + "rules_test": { + "default": [], + "description": "A list of regular expressions of which if matched denotes a test rule", + "items": { + "type": "string" + }, + "type": "array" + }, "weight_cost": { "default": 50, "description": "A value between 0 and 100 that determines the order of resources when multiple can be used", diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.notifications.emergency_bridge.json b/applications/crossbar/priv/couchdb/schemas/kapi.notifications.emergency_bridge.json new file mode 100644 index 00000000000..8284e29efbd --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.notifications.emergency_bridge.json @@ -0,0 +1,135 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.notifications.emergency_bridge", + "description": "AMQP API for notifications.emergency_bridge", + "properties": { + "Account-DB": { + "type": "string" + }, + "Account-ID": { + "type": "string" + }, + "Account-Name": { + "type": "string" + }, + "Attachment-URL": { + "type": "string" + }, + "Authorizing-ID": { + "type": "string" + }, + "Bcc": { + "type": "string" + }, + "Call-ID": { + "type": "string" + }, + "Cc": { + "type": "string" + }, + "Device-ID": { + "type": "string" + }, + "Device-Name": { + "type": "string" + }, + "Device-Owner-ID": { + "type": "string" + }, + "Emergency-Address-City": { + "type": "string" + }, + "Emergency-Address-Latitude": { + "type": "string" + }, + "Emergency-Address-Longitude": { + "type": "string" + }, + "Emergency-Address-Postal-Code": { + "type": "string" + }, + "Emergency-Address-Region": { + "type": "string" + }, + "Emergency-Address-Street-1": { + "type": "string" + }, + "Emergency-Address-Street-2": { + "type": "string" + }, + "Emergency-Caller-ID-Name": { + "type": "string" + }, + "Emergency-Caller-ID-Number": { + "type": "string" + }, + "Emergency-Notfication-Contact-Emails": { + "type": "string" + }, + "Emergency-Test-Call": { + "type": "string" + }, + "Emergency-To-DID": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "notification" + ], + "type": "string" + }, + "Event-Name": { + "enum": [ + "emergency_bridge" + ], + "type": "string" + }, + "From": { + "type": "string" + }, + "HTML": { + "type": "string" + }, + "Outbound-Caller-ID-Name": { + "type": "string" + }, + "Outbound-Caller-ID-Number": { + "type": "string" + }, + "Owner-ID": { + "type": "string" + }, + "Preview": { + "type": "boolean" + }, + "Realm": { + "type": "string" + }, + "Reply-To": { + "type": "string" + }, + "Subject": { + "type": "string" + }, + "Text": { + "type": "string" + }, + "To": { + "type": "string" + }, + "User-Email": { + "type": "string" + }, + "User-First-Name": { + "type": "string" + }, + "User-Last-Name": { + "type": "string" + } + }, + "required": [ + "Account-ID", + "Call-ID" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/phone_numbers.json b/applications/crossbar/priv/couchdb/schemas/phone_numbers.json index df038576cac..235ea584854 100644 --- a/applications/crossbar/priv/couchdb/schemas/phone_numbers.json +++ b/applications/crossbar/priv/couchdb/schemas/phone_numbers.json @@ -89,6 +89,14 @@ "description": "The e911 provisioning system calculated service address longitude", "type": "string" }, + "notification_contact_emails": { + "default": [], + "description": "A list of email addresses to receive notification when this number places an emergency call", + "items": { + "type": "string" + }, + "type": "array" + }, "plus_four": { "description": "The extended zip/postal code where the number is in service", "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/resources.json b/applications/crossbar/priv/couchdb/schemas/resources.json index 2ab8da5621d..3a5049356d0 100644 --- a/applications/crossbar/priv/couchdb/schemas/resources.json +++ b/applications/crossbar/priv/couchdb/schemas/resources.json @@ -418,6 +418,14 @@ }, "type": "array" }, + "rules_test": { + "default": [], + "description": "A list of regular expressions of which if matched denotes a test rule", + "items": { + "type": "string" + }, + "type": "array" + }, "weight_cost": { "default": 50, "description": "A value between 0 and 100 that determines the order of resources when multiple can be used", diff --git a/applications/crossbar/priv/oas3/oas3-schemas.yml b/applications/crossbar/priv/oas3/oas3-schemas.yml index 968e1797dda..6b32abf8bec 100644 --- a/applications/crossbar/priv/oas3/oas3-schemas.yml +++ b/applications/crossbar/priv/oas3/oas3-schemas.yml @@ -6788,6 +6788,13 @@ 'longitude': 'description': The e911 provisioning system calculated service address longitude 'type': string + 'notification_contact_emails': + 'default': [] + 'description': |- + A list of email addresses to receive notification when this number places an emergency call + 'items': + 'type': string + 'type': array 'plus_four': 'description': The extended zip/postal code where the number is in service 'type': string @@ -8244,6 +8251,12 @@ 'items': 'type': string 'type': array + 'rules_test': + 'default': [] + 'description': A list of regular expressions of which if matched denotes a test rule + 'items': + 'type': string + 'type': array 'weight_cost': 'default': 50 'description': |- diff --git a/applications/crossbar/src/modules/cb_notifications.erl b/applications/crossbar/src/modules/cb_notifications.erl index b8ac57339c3..51be1fb8a2d 100644 --- a/applications/crossbar/src/modules/cb_notifications.erl +++ b/applications/crossbar/src/modules/cb_notifications.erl @@ -552,6 +552,8 @@ publish_fun(<<"cnam_request">>) -> fun kapi_notifications:publish_cnam_request/1; publish_fun(<<"customer_update">>) -> fun kapi_notifications:publish_customer_update/1; +publish_fun(<<"emergency_bridge">>) -> + fun kapi_notifications:publish_emergency_bridge/1; publish_fun(<<"denied_emergency_bridge">>) -> fun kapi_notifications:publish_denied_emergency_bridge/1; publish_fun(<<"deregister">>) -> diff --git a/applications/stepswitch/doc/emergency.md b/applications/stepswitch/doc/emergency.md new file mode 100644 index 00000000000..71ed7eacb85 --- /dev/null +++ b/applications/stepswitch/doc/emergency.md @@ -0,0 +1,25 @@ +# Stepswitch Emergency Call Handling + +## Emergency Call Identification + +When stepswitch attempts to execute a bridge of a call, it first checks to see if the call is flagged as an emergency call. If this condition is met, it will publish an `emergency_bridge` message with metadata for the call. + +### Emergency Test Calls + +You can also define [test call rules](rules.md) for the regional emergency services test number. + +Stepswitch will attempt to match the dialed DID of an emergency call against the collected resource endpoints `rules_test` object in an effort to identify test emergency calls. + +## Emergency Call Notification + +If stepswitch identifies an emergency route it immediately publishes the `emergency_bridge` AMQP message in `kapi_notfications`. This message is consumed by the `teletype_emergency_bridge` module as well as configured `notifications` or `emergency_bridge` specific webhooks. + +## Hotdesking + +Stepswitch attempts to identify as much data as possible before publishing an `emergency_bridge`, including resolving the `owner_id` (if present) of a device to a username and email address. If a device does not have an owner, only the device name and id will be resolved. + +Hotdesking allows for multiple users to be hotdesked to a device simultaneously. + +When a device has a single hotdesked user, that user will be resolved as the owner and their metadata published in the notification. + +However, if multiple users are actively hotdesked into a device, a specific owner cannot be determined, so the device is treated as a raw device and user metadata is not published in the notification. diff --git a/applications/stepswitch/doc/rules.md b/applications/stepswitch/doc/rules.md index f76a8da998f..31be39f908a 100644 --- a/applications/stepswitch/doc/rules.md +++ b/applications/stepswitch/doc/rules.md @@ -14,6 +14,9 @@ Example resource document: "rules": [ "^\\+7(\\d{10})$" ], + "rules_test": [ + "^\\+{0,1}(933)$" + ], "cid_rules": [ "^(\\+749[59]\\d{7})$" ], @@ -35,3 +38,9 @@ You can obviously add regexps for specific area codes, toll-free, E911, and inte ## CallerID number rules ("cid_rules" field) Like "rules" field, but capture groups don't modify outgoing CallerID number. If you want modify CallerID number - use "formatters". + +## ("rules_test" field) + +The `rules_test` object defines an array of regular expressions for test patterns of the given resource. + +For example, if the resource handles emergency routes in North America, defining `933` as a test route, will inform teletype this emergency call is a test and will be reflected as such in the notification. diff --git a/applications/stepswitch/src/stepswitch_bridge.erl b/applications/stepswitch/src/stepswitch_bridge.erl index 5a51288d3f6..5c9c8e728e0 100644 --- a/applications/stepswitch/src/stepswitch_bridge.erl +++ b/applications/stepswitch/src/stepswitch_bridge.erl @@ -317,7 +317,9 @@ maybe_bridge(#state{endpoints=Endpoints ,control_queue=ControlQ }=State) -> case contains_emergency_endpoints(Endpoints) of - 'true' -> maybe_bridge_emergency(State); + 'true' -> + _ = send_emergency_bridge_notification(State), + maybe_bridge_emergency(State); 'false' -> Name = bridge_outbound_cid_name(OffnetReq), Number = bridge_outbound_cid_number(OffnetReq), @@ -694,6 +696,113 @@ send_deny_emergency_response(OffnetReq, ControlQ) -> ), kz_call_response:send(CallId, ControlQ, Code, Cause, Media). +-spec send_emergency_bridge_notification(state()) -> 'ok'. +send_emergency_bridge_notification(#state{resource_req=OffnetReq}=State) -> + Setters = [{fun set_emergency_call_meta/2, State} + ,{fun set_emergency_device_meta/2, OffnetReq} + ,{fun set_emergency_address_meta/2, OffnetReq} + ,{fun set_emergency_user_meta/2, OffnetReq} + ,{fun set_default_headers/2, OffnetReq} + ], + + Props = lists:foldl(fun({F, O}, A) -> F(O, A) end, [], Setters), + kapps_notify_publisher:cast(Props, fun kapi_notifications:publish_emergency_bridge/1). + +-spec set_default_headers(kapi_offnet_resource:req(), kz_term:proplist()) -> kz_term:proplist(). +set_default_headers(_OffnetReq, Props) -> + Props ++ kz_api:default_headers(?APP_NAME, ?APP_VERSION). + +-spec set_emergency_call_meta(state(), kz_term:proplist()) -> kz_term:proplist(). +set_emergency_call_meta(#state{resource_req=OffnetReq}=State, Props) -> + [{<<"Call-ID">>, kapi_offnet_resource:call_id(OffnetReq)} + ,{<<"Account-ID">>, kapi_offnet_resource:account_id(OffnetReq)} + ,{?KEY_E_CALLER_ID_NUMBER, kapi_offnet_resource:emergency_caller_id_number(OffnetReq)} + ,{?KEY_E_CALLER_ID_NAME, kapi_offnet_resource:emergency_caller_id_name(OffnetReq)} + ,{?KEY_OUTBOUND_CALLER_ID_NUMBER, kapi_offnet_resource:outbound_caller_id_number(OffnetReq)} + ,{?KEY_OUTBOUND_CALLER_ID_NAME, kapi_offnet_resource:outbound_caller_id_name(OffnetReq)} + ,{<<"Emergency-Test-Call">>, maybe_emergency_test_call(State)} + ,{<<"Emergency-To-DID">>, kapi_offnet_resource:to_did(OffnetReq)} + | Props + ]. + + +-spec set_emergency_device_meta(kapi_offnet_resource:req(), kz_term:proplist()) -> kz_term:proplist(). +set_emergency_device_meta(OffnetReq, Props) -> + AccountID = kapi_offnet_resource:account_id(OffnetReq), + DeviceID = kz_json:get_ne_value(<<"Authorizing-ID">>, kapi_offnet_resource:requestor_custom_channel_vars(OffnetReq), 'undefined'), + OwnerID =kz_json:get_ne_value(<<"Owner-ID">>, kapi_offnet_resource:requestor_custom_channel_vars(OffnetReq), 'undefined'), + {'ok', DeviceJObj} = kzd_devices:fetch(AccountID, DeviceID), + [{<<"Authorizing-ID">>, DeviceID} + ,{<<"Owner-ID">>, OwnerID} + ,{<<"Device-Name">>, kzd_devices:name(DeviceJObj)} + | Props + ]. + +-spec set_emergency_address_meta(kapi_offnet_resource:req(), kz_term:proplist()) -> kz_term:proplist(). +set_emergency_address_meta(OffnetReq, Props) -> + AccountDB = kzs_util:format_account_db(kapi_offnet_resource:account_id(OffnetReq)), + case kz_datamgr:open_doc(AccountDB, kapi_offnet_resource:emergency_caller_id_number(OffnetReq)) of + {'ok', Doc} -> + [{<<"Emergency-Address-Street-1">>, extract_e911_street_address_field(Doc, <<"street_address">>)} + ,{<<"Emergency-Address-Street-2">>, extract_e911_street_address_field(Doc, <<"street_address_extended">>)} + ,{<<"Emergency-Address-City">>, kzd_phone_numbers:e911_locality(Doc)} + ,{<<"Emergency-Address-Latitude">>, kzd_phone_numbers:e911_latitude(Doc)} + ,{<<"Emergency-Address-Longitude">>, kzd_phone_numbers:e911_latitude(Doc)} + ,{<<"Emergency-Address-Region">>, kzd_phone_numbers:e911_region(Doc)} + ,{<<"Emergency-Address-Postal-Code">>, kzd_phone_numbers:e911_postal_code(Doc)} + ,{<<"Emergency-Notfication-Contact-Emails">>, kzd_phone_numbers:e911_notification_contact_emails(Doc)} + | Props + ]; + {'error', _} -> Props + end. + +-spec maybe_emergency_test_call(state()) -> boolean(). +maybe_emergency_test_call(#state{endpoints=Endpoints + ,resource_req=OffnetReq + }=_State) -> + is_resource_test_rule(Endpoints, kapi_offnet_resource:to_did(OffnetReq)). + +-spec is_resource_test_rule(stepswitch_resources:endpoints(), kz_term:ne_binary()) -> boolean(). +is_resource_test_rule(Endpoints, To) -> + is_resource_test_rule(Endpoints, To, 'false'). + +-spec is_resource_test_rule(stepswitch_resources:endpoints(), kz_term:ne_binary(), boolean()) -> boolean(). +is_resource_test_rule([], _To, Acc) -> Acc; +is_resource_test_rule([E | Rest], To, Acc) -> + ResourceId = kz_json:get_ne_value([<<"Custom-Channel-Vars">>, <<"Resource-ID">>], E), + Match = stepswitch_resources:is_test_number(To, ResourceId), + is_resource_test_rule(Rest, To, Match or Acc). + +-spec extract_e911_street_address_field(kz_doc:doc(), kz_term:ne_binary()) -> kz_term:ne_binary() | 'undefined'. +extract_e911_street_address_field(Doc, Key) -> + JObj = kzd_phone_numbers:e911(Doc), + case kz_json:get_ne_value(Key, JObj) of + 'undefined' -> maybe_use_e911_legacy_value(Doc, Key); + Value -> Value + end. + +-spec maybe_use_e911_legacy_value(kz_doc:doc(), kz_term:ne_binary()) -> kz_term:ne_binary() | 'undefined'. +maybe_use_e911_legacy_value(Doc, <<"street_address">>) -> + <<(kzd_phone_numbers:e911_legacy_data_house_number(Doc))/binary + ," " + ,(kzd_phone_numbers:e911_legacy_data_streetname(Doc))/binary>>; +maybe_use_e911_legacy_value(Doc, <<"street_address_extended">>) -> + kzd_phone_numbers:e911_legacy_data_suite(Doc). + +-spec set_emergency_user_meta(kapi_offnet_resource:req(), kz_term:proplist()) -> kz_term:proplist(). +set_emergency_user_meta(OffnetReq, Props) -> + AccountID = kapi_offnet_resource:account_id(OffnetReq), + OwnerID =kz_json:get_ne_value(<<"Owner-ID">>, kapi_offnet_resource:requestor_custom_channel_vars(OffnetReq), 'undefined'), + case kzd_users:fetch(AccountID, OwnerID) of + {'ok', UserJObj} -> + [{<<"User-First-Name">>, kzd_users:first_name(UserJObj)} + ,{<<"User-Last-Name">>, kzd_users:last_name(UserJObj)} + ,{<<"User-Email">>, kzd_users:email(UserJObj)} + | Props + ]; + {'error', _} -> Props + end. + -spec get_event_type(kz_call_event:doc()) -> {kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()}. get_event_type(CallEvt) -> diff --git a/applications/stepswitch/src/stepswitch_resources.erl b/applications/stepswitch/src/stepswitch_resources.erl index dbec9ee1900..d8a1e1b7f8a 100644 --- a/applications/stepswitch/src/stepswitch_resources.erl +++ b/applications/stepswitch/src/stepswitch_resources.erl @@ -19,6 +19,7 @@ -export([maybe_add_proxies/3]). -export([gateways_to_endpoints/4]). -export([check_diversion_fields/1]). +-export([is_test_number/2]). -export([get_resrc_id/1 ,get_resrc_rev/1 @@ -27,7 +28,9 @@ ,get_resrc_grace_period/1 ,get_resrc_flags/1 ,get_resrc_rules/1 + ,get_resrc_rules_test/1 ,get_resrc_raw_rules/1 + ,get_resrc_raw_rules_test/1 ,get_resrc_cid_rules/1 ,get_resrc_cid_raw_rules/1 ,get_resrc_gateways/1 @@ -55,7 +58,9 @@ ,set_resrc_grace_period/2 ,set_resrc_flags/2 ,set_resrc_rules/2 + ,set_resrc_rules_test/2 ,set_resrc_raw_rules/2 + ,set_resrc_raw_rules_test/2 ,set_resrc_cid_rules/2 ,set_resrc_cid_raw_rules/2 ,set_resrc_gateways/2 @@ -136,7 +141,9 @@ ,grace_period = 3 :: non_neg_integer() ,flags = [] :: list() ,rules = [] :: list() + ,rules_test = [] :: list() ,raw_rules = [] :: list() + ,raw_rules_test = [] :: list() ,cid_rules = [] :: list() ,cid_raw_rules = [] :: list() ,gateways = [] :: list() @@ -222,6 +229,7 @@ resource_to_props(#resrc{}=Resource) -> ,{<<"Flags">>, Resource#resrc.flags} ,{<<"Codecs">>, Resource#resrc.codecs} ,{<<"Rules">>, Resource#resrc.raw_rules} + ,{<<"Rules-Test">>, Resource#resrc.raw_rules_test} ,{<<"Caller-ID-Rules">>, Resource#resrc.cid_raw_rules} ,{<<"Formatters">>, Resource#resrc.formatters} ,{<<"Privacy-Method">>, Resource#resrc.privacy_method} @@ -246,6 +254,23 @@ endpoints(Number, OffnetJObj) -> Endpoints -> sort_endpoints(Endpoints) end. +-spec is_test_number(kz_term:ne_binary(), kz_term:ne_binary()) -> boolean(). +is_test_number(Number, ResourceId) -> + case get_resource(ResourceId) of + 'undefined' -> 'false'; + Resource -> + Rules = get_resrc_rules_test(Resource), + maybe_match_test_number(Rules, Number) + end. + +-spec maybe_match_test_number([re:mp()] , kz_term:ne_binary()) -> boolean(). +maybe_match_test_number([], _) -> 'false'; +maybe_match_test_number([Rule | Rules], Number) -> + case re:run(Number, Rule) of + {'match', _Captured} -> 'true'; + 'nomatch' -> maybe_match_test_number(Rules, Number) + end. + -spec maybe_get_endpoints(kz_term:ne_binary(), kapi_offnet_resource:req()) -> endpoints(). maybe_get_endpoints(Number, OffnetJObj) -> case kapi_offnet_resource:hunt_account_id(OffnetJObj) of @@ -1091,7 +1116,9 @@ resource_from_jobj(JObj) -> ,from_account_realm=kzd_resources:from_account_realm(JObj) ,fax_option=kzd_resources:media_fax_option(JObj) ,raw_rules=kzd_resources:rules(JObj, []) + ,raw_rules_test=kzd_resources:rules_test(JObj, []) ,rules=resource_rules(JObj) + ,rules_test=resource_rules_test(JObj) ,cid_raw_rules=kzd_resources:cid_rules(JObj, []) ,cid_rules=resource_cid_rules(JObj) ,weight=resource_weight(JObj) @@ -1152,7 +1179,14 @@ resource_rules([Rule|Rules], CompiledRules) -> resource_rules(Rules, CompiledRules) end. --spec resource_cid_rules(kzd_resources:doc()) -> rules(). +-spec resource_rules_test(kz_json:object()) -> rules(). +resource_rules_test(JObj) -> + Rules = kz_json:get_value(<<"rules_test">>, JObj, []), + lager:info("compiling resource test rules for ~s / ~s: ~p" + ,[kz_doc:account_db(JObj, <<"offnet">>), kz_doc:id(JObj), Rules]), + resource_rules(Rules, []). + +-spec resource_cid_rules(kz_json:object()) -> rules(). resource_cid_rules(ResourceJObj) -> lager:info("compiling caller id rules for ~s / ~s" ,[kz_doc:account_db(ResourceJObj, <<"offnet">>), kz_doc:id(ResourceJObj)] @@ -1319,9 +1353,15 @@ get_resrc_flags(#resrc{flags=Flags}) -> Flags. -spec get_resrc_rules(resource()) -> list(). get_resrc_rules(#resrc{rules=Rules}) -> Rules. +-spec get_resrc_rules_test(resource()) -> list(). +get_resrc_rules_test(#resrc{rules_test=Rules}) -> Rules. + -spec get_resrc_raw_rules(resource()) -> list(). get_resrc_raw_rules(#resrc{raw_rules=RawRules}) -> RawRules. +-spec get_resrc_raw_rules_test(resource()) -> list(). +get_resrc_raw_rules_test(#resrc{raw_rules_test=RawRules}) -> RawRules. + -spec get_resrc_cid_rules(resource()) -> list(). get_resrc_cid_rules(#resrc{cid_rules=CIDRules}) -> CIDRules. @@ -1398,9 +1438,15 @@ set_resrc_flags(Resource, Flags) -> Resource#resrc{flags=Flags}. -spec set_resrc_rules(resource(), list()) -> resource(). set_resrc_rules(Resource, Rules) -> Resource#resrc{rules=Rules}. +-spec set_resrc_rules_test(resource(), list()) -> resource(). +set_resrc_rules_test(Resource, Rules) -> Resource#resrc{rules_test=Rules}. + -spec set_resrc_raw_rules(resource(), list()) -> resource(). set_resrc_raw_rules(Resource, RawRules) -> Resource#resrc{raw_rules=RawRules}. +-spec set_resrc_raw_rules_test(resource(), list()) -> resource(). +set_resrc_raw_rules_test(Resource, RawRules) -> Resource#resrc{raw_rules_test=RawRules}. + -spec set_resrc_cid_rules(resource(), list()) -> resource(). set_resrc_cid_rules(Resource, CIDRules) -> Resource#resrc{cid_rules=CIDRules}. diff --git a/applications/teletype/doc/README.md b/applications/teletype/doc/README.md index 2078d68217b..7cef39f062b 100644 --- a/applications/teletype/doc/README.md +++ b/applications/teletype/doc/README.md @@ -5,3 +5,19 @@ Teletype listens for events within Kazoo and notifies those that are interested. ## Autostart modules The teletype modules listed in the `autoload_modules` section of `system_config/notify` will automatically be started when teletype is started. + +## Emergency Call Notification + +The `emergency_bridge` notification is triggered when stepswitch recognizes an emergency call is being attempted. + +Teletype will attempt to fetch the `emergency_notfication_contact_emails` parameter from the which is set on a phone number's `e911.notification_contact_emails`, but will fallback to sending it to the Admins if this parameter has not been explictly set. + +### Hotdesking + +Stepswitch attempts to identify as much data as possible before publishing an `emergency_bridge`, including resolving the `owner_id` (if present) of a device to a username and email address. If a device does not have an owner, only the device name and id will be resolved. + +Hotdesking allows for multiple users to be hotdesked to a device simultaneously. + +When a device has a single hotdesked user, that user will be resolved as the owner and their metadata published in the notification. + +However, if multiple users are actively hotdesked into a device, a specific owner cannot be determined, so the device is treated as a raw device and user metadata is not published in the notification. diff --git a/applications/teletype/priv/templates/emergency_bridge.html b/applications/teletype/priv/templates/emergency_bridge.html new file mode 100644 index 00000000000..c60a4fd3cb6 --- /dev/null +++ b/applications/teletype/priv/templates/emergency_bridge.html @@ -0,0 +1,115 @@ + + + + + + + + Emergency Call + + + + +
+
+ Attention! An Emergency {% if call.emergency_test_call %}Test{% endif %} Call has been placed - +
+ +
+ + diff --git a/applications/teletype/priv/templates/emergency_bridge.text b/applications/teletype/priv/templates/emergency_bridge.text new file mode 100644 index 00000000000..5535dd15853 --- /dev/null +++ b/applications/teletype/priv/templates/emergency_bridge.text @@ -0,0 +1,46 @@ + Emergency {% if call.emergency_test_call %}Test{% endif %} Call Notification + +{% if call.owner_id %}{{call.user_first_name}} {{call.user_last_name}}{% else %}The device {{call.device_name}}{% endif %} has just placed an Emergency {% if call.emergency_test_call %}Test{% endif %} Call to {{call.emergency_to_did}} from Account {{account.name}}. Here are the emergency call details: + +=== Emergency {% if call.emergency_test_call %}Test{% endif %} Call Details === + + Call-ID: {{call.call_id}} + + Emergency Service Address: + {% if call.emergency_address_street_1 %} + {{call.emergency_address_street_1}} + {{call.emergency_address_street_2}} + {{call.emergency_address_city}}, {{call.emergency_address_region}} {{call.emergency_address_postal_code}} + {% endif %} + + Emergency Caller ID: + - Name: {{call.emergency_caller_id_name}} + - Number: {{call.emergency_caller_id_number}} + + + External Caller ID: + - Name: {{call.outbound_caller_id_name}} + - Number: {{call.outbound_caller_id_number}} + + Device: + - Device ID: {{call.authorizing_id}} + - Device Name: {{call.device_name}} + {% if call.owner_id %} + - Owner Name: {{call.user_first_name}} {{call.user_last_name}} ({{call.owner_id}}) + - Owner Email: {{call.user_email}} + {% endif %} + + +Account Information + + Account ID: {{account.id}} + Account Name: {{account.name}} + Account Realm: {{account.realm}} + + + +Sent from {{system.encoded_node}} + + + + diff --git a/applications/teletype/src/templates/teletype_emergency_bridge.erl b/applications/teletype/src/templates/teletype_emergency_bridge.erl new file mode 100644 index 00000000000..5106a565a0d --- /dev/null +++ b/applications/teletype/src/templates/teletype_emergency_bridge.erl @@ -0,0 +1,126 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2014-2020, 2600Hz +%%% @doc +%%% @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(teletype_emergency_bridge). + +-export([init/0 + ,handle_req/1 + ]). + +-include("teletype.hrl"). + +-define(TEMPLATE_ID, <<"emergency_bridge">>). + +-define(TEMPLATE_MACROS + ,kz_json:from_list( + [?MACRO_VALUE(<<"call.outbound_caller_id_name">>, <<"outbound_caller_id_name">>, <<"Outbound Caller ID Name">>, <<"Outbound Caller ID Name">>) + ,?MACRO_VALUE(<<"call.outbound_caller_id_number">>, <<"outbound_caller_id_number">>, <<"Outbound Caller ID Number">>, <<"Outbound Caller ID Number">>) + ,?MACRO_VALUE(<<"call.emergency_caller_id_name">>, <<"emergency_caller_id_name">>, <<"Emergency Caller ID Name">>, <<"Emergency Caller ID Name">>) + ,?MACRO_VALUE(<<"call.emergency_caller_id_number">>, <<"emergency_caller_id_number">>, <<"Emergency Caller ID Number">>, <<"Emergency Caller ID Number">>) + ,?MACRO_VALUE(<<"call.call_id">>, <<"call_id">>, <<"Call ID">>, <<"Call ID">>) + ,?MACRO_VALUE(<<"call.device_name">>, <<"device_name">>, <<"Device Name">>, <<"Device Name">>) + ,?MACRO_VALUE(<<"call.emergency_address_street_1">>, <<"emergency_address_street_1">>, <<"Emergency Address Street 1">>, <<"Emergency Address Street 1">>) + ,?MACRO_VALUE(<<"call.emergency_address_street_2">>, <<"emergency_address_street_2">>, <<"Emergency Address Street 2">>, <<"Emergency Address Street 2">>) + ,?MACRO_VALUE(<<"call.emergency_address_city">>, <<"emergency_address_city">>, <<"Emergency Address City">>, <<"Emergency Address City">>) + ,?MACRO_VALUE(<<"call.emergency_address_region">>, <<"emergency_address_region">>, <<"Emergency Address Region">>, <<"Emergency Address Region">>) + ,?MACRO_VALUE(<<"call.emergency_address_postal_code">>, <<"emergency_address_postal_code">>, <<"Emergency Address Postal Code">>, <<"Emergency Address Postal Code">>) + ,?MACRO_VALUE(<<"call.emergency_notification_contact_emails">>, <<"emergency_notification_contact_emails">>, <<"Emergency Contact Emails">>, <<"Emergency Contact Emails">>) + ,?MACRO_VALUE(<<"call.emergency_test_call">>, <<"emergency_test_call">>, <<"Emergency Test Call">>, <<"Emergency Test Call">>) + ,?MACRO_VALUE(<<"call.emergency_to_did">>, <<"emergency_to_did">>, <<"Emergency To DID">>, <<"Emergency To DID">>) + ,?MACRO_VALUE(<<"call.owner_id">>, <<"owner_id">>, <<"Owner ID">>, <<"Owner ID">>) + ,?MACRO_VALUE(<<"call.user_email">>, <<"user_email">>, <<"User Email">>, <<"User Email">>) + ,?MACRO_VALUE(<<"call.user_first_name">>, <<"user_first_name">>, <<"User First Name">>, <<"User First Name">>) + ,?MACRO_VALUE(<<"call.user_last_name">>, <<"user_last_name">>, <<"User Last Name">>, <<"User Last Name">>) + | ?COMMON_TEMPLATE_MACROS + ] + ) + ). + +-define(TEMPLATE_SUBJECT, <<"Emergency{%if call.emergency_test_call %} Test{% endif %} Call Notification from Account '{{account.name}}'">>). +-define(TEMPLATE_CATEGORY, <<"account">>). +-define(TEMPLATE_NAME, <<"Emergency Bridge">>). + +-define(TEMPLATE_TO, ?CONFIGURED_EMAILS(?EMAIL_ADMINS)). +-define(TEMPLATE_FROM, teletype_util:default_from_address()). +-define(TEMPLATE_CC, ?CONFIGURED_EMAILS(?EMAIL_SPECIFIED, [])). +-define(TEMPLATE_BCC, ?CONFIGURED_EMAILS(?EMAIL_SPECIFIED, [])). +-define(TEMPLATE_REPLY_TO, teletype_util:default_reply_to()). + +-spec init() -> 'ok'. +init() -> + kz_log:put_callid(?MODULE), + teletype_templates:init(?TEMPLATE_ID, [{'macros', ?TEMPLATE_MACROS} + ,{'subject', ?TEMPLATE_SUBJECT} + ,{'category', ?TEMPLATE_CATEGORY} + ,{'friendly_name', ?TEMPLATE_NAME} + ,{'to', ?TEMPLATE_TO} + ,{'from', ?TEMPLATE_FROM} + ,{'cc', ?TEMPLATE_CC} + ,{'bcc', ?TEMPLATE_BCC} + ,{'reply_to', ?TEMPLATE_REPLY_TO} + ]), + teletype_bindings:bind(<<"emergency_bridge">>, ?MODULE, 'handle_req'). + +-spec handle_req(kz_json:object()) -> template_response(). +handle_req(JObj) -> + handle_req(JObj, kapi_notifications:emergency_bridge_v(JObj)). + +-spec handle_req(kz_json:object(), boolean()) -> template_response(). +handle_req(_, 'false') -> + lager:debug("invalid data for ~s", [?TEMPLATE_ID]), + teletype_util:notification_failed(?TEMPLATE_ID, <<"validation_failed">>); +handle_req(JObj, 'true') -> + lager:debug("valid data for ~s, processing...", [?TEMPLATE_ID]), + + %% Gather data for template + DataJObj = kz_json:normalize(JObj), + AccountId = kz_json:get_value(<<"account_id">>, DataJObj), + + case teletype_util:is_notice_enabled(AccountId, JObj, ?TEMPLATE_ID) of + 'false' -> teletype_util:notification_disabled(DataJObj, ?TEMPLATE_ID); + 'true' -> process_req(DataJObj) + end. + +-spec process_req(kz_json:object()) -> template_response(). +process_req(DataJObj) -> + Macros = [{<<"system">>, teletype_util:system_params()} + ,{<<"account">>, teletype_util:account_params(DataJObj)} + ,{<<"call">>, kz_json:to_proplist(kz_api:remove_defaults(DataJObj))} + ], + %% Load templates + Templates = teletype_templates:render(?TEMPLATE_ID, Macros, DataJObj), + + + %% Populate templates + RenderedTemplates = [{ContentType, teletype_util:render(?TEMPLATE_ID, Template, Macros)} + || {ContentType, Template} <- Templates + ], + + {'ok', TemplateMetaJObj} = teletype_templates:fetch_notification(?TEMPLATE_ID, kapi_notifications:account_id(DataJObj)), + + Subject = teletype_util:render_subject(kz_json:find(<<"subject">>, [DataJObj, TemplateMetaJObj]) + ,Macros + ), + + Emails = maybe_update_to(DataJObj, TemplateMetaJObj), + + case teletype_util:send_email(Emails, Subject, RenderedTemplates) of + 'ok' -> teletype_util:notification_completed(?TEMPLATE_ID); + {'error', Reason} -> teletype_util:notification_failed(?TEMPLATE_ID, Reason) + end. + +-spec maybe_update_to(kz_json:object(), kz_json:object()) -> email_map(). +maybe_update_to(DataJObj, TemplateMetaJObj) -> + Emails = teletype_util:find_addresses(DataJObj, TemplateMetaJObj, ?TEMPLATE_ID), + case kz_json:get_list_value(<<"emergency_notfication_contact_emails">>, DataJObj, []) of + [] -> Emails; + ToList -> lists:keyreplace(<<"to">>, 1, Emails, {<<"to">>, ToList}) + end. diff --git a/applications/teletype/test/teletype_render_tests.erl b/applications/teletype/test/teletype_render_tests.erl index 639e362f884..b3c5e747b01 100644 --- a/applications/teletype/test/teletype_render_tests.erl +++ b/applications/teletype/test/teletype_render_tests.erl @@ -23,13 +23,14 @@ render_test_() -> ,fun setup/0 ,fun cleanup/1 ,fun(_ReturnOfSetup) -> - [?_assertEqual(38, length(?DEFAULT_MODULES)) + [?_assertEqual(39, length(?DEFAULT_MODULES)) %% ,test_rendering(teletype_account_zone_change) ,test_rendering(teletype_bill_reminder) %% ,test_rendering(teletype_cnam_request) %% ,test_rendering(teletype_customer_update) %% ,test_rendering(teletype_denied_emergency_bridge) ,test_rendering(teletype_deregister) + %% ,test_rendering(teletype_emergency_bridge) %% ,test_rendering(teletype_fax_inbound_error_to_email) %% ,test_rendering(teletype_fax_inbound_to_email) %% ,test_rendering(teletype_fax_outbound_error_to_email) diff --git a/core/kazoo_amqp/src/api/kapi_dialplan.erl b/core/kazoo_amqp/src/api/kapi_dialplan.erl index 7d07c5de26d..03b256bc498 100644 --- a/core/kazoo_amqp/src/api/kapi_dialplan.erl +++ b/core/kazoo_amqp/src/api/kapi_dialplan.erl @@ -414,17 +414,18 @@ tone_detect_v(JObj) -> tone_detect_v(kz_json:to_proplist(JObj)). %% @end %%------------------------------------------------------------------------------ -spec queue(kz_term:api_terms()) -> api_formatter_return(). -queue(Prop) when is_list(Prop) -> - case queue_v(Prop) of - 'true' -> kz_api:build_message(Prop, ?QUEUE_REQ_HEADERS, ?OPTIONAL_QUEUE_REQ_HEADERS); - 'false' -> {'error', "Proplist failed validation for queue_req"} - end; -queue(JObj) -> queue(kz_json:to_proplist(JObj)). +queue(API) -> + queue(API, queue_v(API)). + +-spec queue(kz_term:api_terms(), boolean()) -> api_formatter_return(). +queue(API, 'true') -> + kz_api:build_message(API, ?QUEUE_REQ_HEADERS, ?OPTIONAL_QUEUE_REQ_HEADERS); +queue(_API, 'false') -> + {'error', "Proplist failed validation for queue_req"}. -spec queue_v(kz_term:api_terms()) -> boolean(). -queue_v(Prop) when is_list(Prop) -> - kz_api:validate(Prop, ?QUEUE_REQ_HEADERS, ?QUEUE_REQ_VALUES, ?QUEUE_REQ_TYPES); -queue_v(JObj) -> queue_v(kz_json:to_proplist(JObj)). +queue_v(API) -> + kz_api:validate(API, ?QUEUE_REQ_HEADERS, ?QUEUE_REQ_VALUES, ?QUEUE_REQ_TYPES). %%------------------------------------------------------------------------------ %% @doc Play media. diff --git a/core/kazoo_amqp/src/api/kapi_notifications.erl b/core/kazoo_amqp/src/api/kapi_notifications.erl index ad05519efb4..cf4d013dfc8 100644 --- a/core/kazoo_amqp/src/api/kapi_notifications.erl +++ b/core/kazoo_amqp/src/api/kapi_notifications.erl @@ -135,6 +135,11 @@ ,publish_denied_emergency_bridge/1 ,publish_denied_emergency_bridge/2 ]). +-export([emergency_bridge/1 + ,emergency_bridge_v/1 + ,publish_emergency_bridge/1 + ,publish_emergency_bridge/2 + ]). %% SIP notifications -export([deregister/1 ,deregister_v/1 @@ -1077,6 +1082,52 @@ denied_emergency_bridge_definition() -> ], kapi_definition:setters(Setters). +-spec emergency_bridge_definition() -> kapi_definition:api(). +emergency_bridge_definition() -> + EventName = <<"emergency_bridge">>, + Category = <<"registration">>, + Setters = [{fun kapi_definition:set_name/2, EventName} + ,{fun kapi_definition:set_friendly_name/2, <<"Emergency Call Placed">>} + ,{fun kapi_definition:set_description/2 + ,<<"This event is triggered when a call to an number classified as emergency is placed">> + } + ,{fun kapi_definition:set_category/2, Category} + ,{fun kapi_definition:set_build_fun/2, fun emergency_bridge/1} + ,{fun kapi_definition:set_validate_fun/2, fun emergency_bridge_v/1} + ,{fun kapi_definition:set_publish_fun/2, fun publish_emergency_bridge/1} + ,{fun kapi_definition:set_binding/2, ?BINDING_STRING(Category, <<"emergency_bridge">>)} + ,{fun kapi_definition:set_restrict_to/2, 'emergency_bridge'} + ,{fun kapi_definition:set_required_headers/2, [<<"Account-ID">> + ,<<"Call-ID">> + ]} + ,{fun kapi_definition:set_optional_headers/2, [<<"Account-Name">> + ,<<"Authorizing-ID">> + ,<<"Device-Name">> + ,<<"Emergency-To-DID">> + ,<<"Emergency-Test-Call">> + ,<<"Emergency-Caller-ID-Name">> + ,<<"Emergency-Caller-ID-Number">> + ,<<"Emergency-Address-Street-1">> + ,<<"Emergency-Address-Street-2">> + ,<<"Emergency-Address-City">> + ,<<"Emergency-Address-Region">> + ,<<"Emergency-Address-Postal-Code">> + ,<<"Emergency-Address-Latitude">> + ,<<"Emergency-Address-Longitude">> + ,<<"Emergency-Notfication-Contact-Emails">> + ,<<"Outbound-Caller-ID-Name">> + ,<<"Outbound-Caller-ID-Number">> + ,<<"Owner-ID">> + ,<<"User-First-Name">> + ,<<"User-Last-Name">> + ,<<"User-Email">> + | ?DEFAULT_OPTIONAL_HEADERS + ]} + ,{fun kapi_definition:set_values/2, ?NOTIFY_VALUES(EventName)} + ,{fun kapi_definition:set_types/2, []} + ], + kapi_definition:setters(Setters). + %%%============================================================================= %%% SIP Notifications Definitions %%%============================================================================= @@ -1710,6 +1761,7 @@ api_definitions() -> ,port_unconfirmed_definition() ,ported_definition() ,denied_emergency_bridge_definition() + ,emergency_bridge_definition() ,deregister_definition() ,first_occurrence_definition() ,missed_call_definition() @@ -1783,6 +1835,8 @@ api_definition(<<"ported">>) -> ported_definition(); api_definition(<<"denied_emergency_bridge">>) -> denied_emergency_bridge_definition(); +api_definition(<<"emergency_bridge">>) -> + emergency_bridge_definition(); api_definition(<<"deregister">>) -> deregister_definition(); api_definition(<<"first_occurrence">>) -> @@ -2615,6 +2669,31 @@ publish_denied_emergency_bridge(API, ContentType) -> ), kz_amqp_util:notifications_publish(kapi_definition:binding(Definition), Payload, ContentType). +%%------------------------------------------------------------------------------ +%% @doc Takes proplist, creates JSON string and publish it on AMQP. +%% @end +%%------------------------------------------------------------------------------ +-spec emergency_bridge(kz_term:api_terms()) -> api_formatter_return(). +emergency_bridge(Prop) -> + kapi_definition:build_message(Prop, emergency_bridge_definition()). + +-spec emergency_bridge_v(kz_term:api_terms()) -> boolean(). +emergency_bridge_v(Prop) -> + kapi_definition:validate(Prop, emergency_bridge_definition()). + +-spec publish_emergency_bridge(kz_term:api_terms()) -> 'ok'. +publish_emergency_bridge(JObj) -> + publish_emergency_bridge(JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_emergency_bridge(kz_term:api_terms(), kz_term:ne_binary()) -> 'ok'. +publish_emergency_bridge(API, ContentType) -> + Definition = emergency_bridge_definition(), + {'ok', Payload} = kz_api:prepare_api_payload(API + ,kapi_definition:values(Definition) + ,kapi_definition:build_fun(Definition) + ), + kz_amqp_util:notifications_publish(kapi_definition:binding(Definition), Payload, ContentType). + %%%============================================================================= %%% Register Notifications Functions %%%============================================================================= diff --git a/core/kazoo_amqp/src/api/kz_api.erl b/core/kazoo_amqp/src/api/kz_api.erl index dab2a33a638..ec64fa566f2 100644 --- a/core/kazoo_amqp/src/api/kz_api.erl +++ b/core/kazoo_amqp/src/api/kz_api.erl @@ -178,7 +178,7 @@ reply_to(JObj) -> default_headers(AppName, AppVsn) -> default_headers('undefined', AppName, AppVsn). --spec default_headers(kz_term:api_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:proplist(). +-spec default_headers(kz_term:api_ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:proplist(). default_headers(ServerID, AppName, AppVsn) -> [{?KEY_SERVER_ID, ServerID} ,{?KEY_APP_NAME, AppName} @@ -186,11 +186,11 @@ default_headers(ServerID, AppName, AppVsn) -> ,{?KEY_NODE, kz_term:to_binary(node())} ]. --spec default_headers(kz_term:api_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:proplist(). +-spec default_headers(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:proplist(). default_headers(EvtCat, EvtName, AppName, AppVsn) -> - default_headers(<<>>, EvtCat, EvtName, AppName, AppVsn). + default_headers('undefined', EvtCat, EvtName, AppName, AppVsn). --spec default_headers(kz_term:api_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:proplist(). +-spec default_headers(kz_term:api_ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:proplist(). default_headers(ServerID, EvtCat, EvtName, AppName, AppVsn) -> props:filter_empty( [{?KEY_SERVER_ID, ServerID} @@ -202,8 +202,6 @@ default_headers(ServerID, EvtCat, EvtName, AppName, AppVsn) -> ]). -spec default_headers_v(kz_term:api_terms()) -> boolean(). - - default_headers_v(Props) when is_list(Props) -> Filtered = props:filter_empty(Props), lists:all(fun(K) -> default_header_v(K, Filtered) end, ?DEFAULT_HEADERS); diff --git a/core/kazoo_apps/src/kz_amqp_worker.erl b/core/kazoo_apps/src/kz_amqp_worker.erl index a03a91f045a..911c2ee5c41 100644 --- a/core/kazoo_apps/src/kz_amqp_worker.erl +++ b/core/kazoo_apps/src/kz_amqp_worker.erl @@ -567,7 +567,7 @@ request_filter(Props) -> props:filter(fun request_proplist_filter/1, Props). -spec request_proplist_filter({kz_term:proplist_key(), kz_term:proplist_value()}) -> boolean(). -request_proplist_filter({<<"Server-ID">>, Value}) -> +request_proplist_filter({?KEY_SERVER_ID, Value}) -> not kz_term:is_empty(Value); request_proplist_filter({_, 'undefined'}) -> 'false'; request_proplist_filter(_) -> 'true'. @@ -1034,7 +1034,7 @@ maybe_convert_to_proplist(Req) -> maybe_set_msg_id(Props) -> case kz_api:msg_id(Props) of 'undefined' -> - props:set_value(<<"Msg-ID">>, kz_binary:rand_hex(8), Props); + props:set_value(?KEY_MSG_ID, kz_binary:rand_hex(8), Props); _MsgId -> Props end. diff --git a/core/kazoo_call/src/kz_call_response.erl b/core/kazoo_call/src/kz_call_response.erl index 50a0a3f34b8..a380d8b28af 100644 --- a/core/kazoo_call/src/kz_call_response.erl +++ b/core/kazoo_call/src/kz_call_response.erl @@ -111,12 +111,12 @@ do_send(CallId, CtrlQ, Commands) -> ,{<<"Msg-ID">>, kz_binary:rand_hex(6)} | kz_api:default_headers(<<"call">>, <<"command">>, <<"call_response">>, <<"0.1.0">>) ], - kz_amqp_worker:cast(Command - ,fun(C) -> - {'ok', Payload} = kapi_dialplan:queue(C), - kapi_dialplan:publish_action(CtrlQ, Payload) - end - ). + kz_amqp_worker:cast(Command, fun(C) -> publish_queue_command(CtrlQ, C) end). + +-spec publish_queue_command(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. +publish_queue_command(CtrlQ, Command) -> + {'ok', Payload} = kapi_dialplan:queue(Command), + kapi_dialplan:publish_action(CtrlQ, Payload). %%------------------------------------------------------------------------------ %% @doc diff --git a/core/kazoo_documents/src/kzd_phone_numbers.erl b/core/kazoo_documents/src/kzd_phone_numbers.erl index a0d4d0939bb..4c49a212bc8 100644 --- a/core/kazoo_documents/src/kzd_phone_numbers.erl +++ b/core/kazoo_documents/src/kzd_phone_numbers.erl @@ -48,6 +48,7 @@ -export([e911_locality/1, e911_locality/2, set_e911_locality/2]). -export([e911_location_id/1, e911_location_id/2, set_e911_location_id/2]). -export([e911_longitude/1, e911_longitude/2, set_e911_longitude/2]). +-export([e911_notification_contact_emails/1, e911_notification_contact_emails/2, set_e911_notification_contact_emails/2]). -export([e911_plus_four/1, e911_plus_four/2, set_e911_plus_four/2]). -export([e911_postal_code/1, e911_postal_code/2, set_e911_postal_code/2]). -export([e911_region/1, e911_region/2, set_e911_region/2]). @@ -579,6 +580,18 @@ e911_longitude(Doc, Default) -> set_e911_longitude(Doc, E911Longitude) -> kz_json:set_value([<<"e911">>, <<"longitude">>], E911Longitude, Doc). +-spec e911_notification_contact_emails(doc()) -> kz_term:ne_binaries(). +e911_notification_contact_emails(Doc) -> + e911_notification_contact_emails(Doc, []). + +-spec e911_notification_contact_emails(doc(), Default) -> kz_term:ne_binaries() | Default. +e911_notification_contact_emails(Doc, Default) -> + kz_json:get_list_value([<<"e911">>, <<"notification_contact_emails">>], Doc, Default). + +-spec set_e911_notification_contact_emails(doc(), kz_term:ne_binaries()) -> doc(). +set_e911_notification_contact_emails(Doc, E911NotificationContactEmails) -> + kz_json:set_value([<<"e911">>, <<"notification_contact_emails">>], E911NotificationContactEmails, Doc). + -spec e911_plus_four(doc()) -> kz_term:api_binary(). e911_plus_four(Doc) -> e911_plus_four(Doc, 'undefined'). diff --git a/core/kazoo_documents/src/kzd_phone_numbers.erl.src b/core/kazoo_documents/src/kzd_phone_numbers.erl.src index 8750c9c76b9..8063671928b 100644 --- a/core/kazoo_documents/src/kzd_phone_numbers.erl.src +++ b/core/kazoo_documents/src/kzd_phone_numbers.erl.src @@ -24,6 +24,7 @@ -export([e911_locality/1, e911_locality/2, set_e911_locality/2]). -export([e911_location_id/1, e911_location_id/2, set_e911_location_id/2]). -export([e911_longitude/1, e911_longitude/2, set_e911_longitude/2]). +-export([e911_notification_contact_emails/1, e911_notification_contact_emails/2, set_e911_notification_contact_emails/2]). -export([e911_plus_four/1, e911_plus_four/2, set_e911_plus_four/2]). -export([e911_postal_code/1, e911_postal_code/2, set_e911_postal_code/2]). -export([e911_region/1, e911_region/2, set_e911_region/2]). @@ -272,6 +273,18 @@ e911_longitude(Doc, Default) -> set_e911_longitude(Doc, E911Longitude) -> kz_json:set_value([<<"e911">>, <<"longitude">>], E911Longitude, Doc). +-spec e911_notification_contact_emails(doc()) -> kz_term:ne_binaries(). +e911_notification_contact_emails(Doc) -> + e911_notification_contact_emails(Doc, []). + +-spec e911_notification_contact_emails(doc(), Default) -> kz_term:ne_binaries() | Default. +e911_notification_contact_emails(Doc, Default) -> + kz_json:get_list_value([<<"e911">>, <<"notification_contact_emails">>], Doc, Default). + +-spec set_e911_notification_contact_emails(doc(), kz_term:ne_binaries()) -> doc(). +set_e911_notification_contact_emails(Doc, E911NotificationContactEmails) -> + kz_json:set_value([<<"e911">>, <<"notification_contact_emails">>], E911NotificationContactEmails, Doc). + -spec e911_plus_four(doc()) -> kz_term:api_binary(). e911_plus_four(Doc) -> e911_plus_four(Doc, 'undefined'). diff --git a/core/kazoo_documents/src/kzd_resources.erl b/core/kazoo_documents/src/kzd_resources.erl index ec42eaa669f..df46e0e9fae 100644 --- a/core/kazoo_documents/src/kzd_resources.erl +++ b/core/kazoo_documents/src/kzd_resources.erl @@ -39,6 +39,7 @@ -export([name/1, name/2, set_name/2]). -export([require_flags/1, require_flags/2, set_require_flags/2]). -export([rules/1, rules/2, set_rules/2]). +-export([rules_test/1, rules_test/2, set_rules_test/2]). -export([weight_cost/1, weight_cost/2, set_weight_cost/2]). -export([media_fax_option/1, media_fax_option/2 @@ -391,6 +392,18 @@ rules(Doc, Default) -> set_rules(Doc, Rules) -> kz_json:set_value([<<"rules">>], Rules, Doc). +-spec rules_test(doc()) -> kz_term:ne_binaries(). +rules_test(Doc) -> + rules_test(Doc, []). + +-spec rules_test(doc(), Default) -> kz_term:ne_binaries() | Default. +rules_test(Doc, Default) -> + kz_json:get_list_value([<<"rules_test">>], Doc, Default). + +-spec set_rules_test(doc(), kz_term:ne_binaries()) -> doc(). +set_rules_test(Doc, RulesTest) -> + kz_json:set_value([<<"rules_test">>], RulesTest, Doc). + -spec weight_cost(doc()) -> integer(). weight_cost(Doc) -> weight_cost(Doc, 50). diff --git a/core/kazoo_documents/src/kzd_resources.erl.src b/core/kazoo_documents/src/kzd_resources.erl.src index bd439b90e6d..6227ec32d51 100644 --- a/core/kazoo_documents/src/kzd_resources.erl.src +++ b/core/kazoo_documents/src/kzd_resources.erl.src @@ -28,6 +28,7 @@ -export([name/1, name/2, set_name/2]). -export([require_flags/1, require_flags/2, set_require_flags/2]). -export([rules/1, rules/2, set_rules/2]). +-export([rules_test/1, rules_test/2, set_rules_test/2]). -export([weight_cost/1, weight_cost/2, set_weight_cost/2]). @@ -306,6 +307,18 @@ rules(Doc, Default) -> set_rules(Doc, Rules) -> kz_json:set_value([<<"rules">>], Rules, Doc). +-spec rules_test(doc()) -> kz_term:ne_binaries(). +rules_test(Doc) -> + rules_test(Doc, []). + +-spec rules_test(doc(), Default) -> kz_term:ne_binaries() | Default. +rules_test(Doc, Default) -> + kz_json:get_list_value([<<"rules_test">>], Doc, Default). + +-spec set_rules_test(doc(), kz_term:ne_binaries()) -> doc(). +set_rules_test(Doc, RulesTest) -> + kz_json:set_value([<<"rules_test">>], RulesTest, Doc). + -spec weight_cost(doc()) -> integer(). weight_cost(Doc) -> weight_cost(Doc, 50). diff --git a/core/kazoo_stdlib/src/props.erl b/core/kazoo_stdlib/src/props.erl index 160c0c4a0da..0ec90dcd71d 100644 --- a/core/kazoo_stdlib/src/props.erl +++ b/core/kazoo_stdlib/src/props.erl @@ -94,7 +94,7 @@ insert_values(KVs, Props) -> lists:foldl(fun insert_value/2, Props, KVs). %% replaces value of Key with Value if Key exists; otherwise Props is unchanged --spec replace_value(any(), any(), kz_term:proplist()) -> kz_term:proplist(). +-spec replace_value(kz_term:proplist_key(), kz_term:proplist_value(), kz_term:proplist()) -> kz_term:proplist(). replace_value(Key, Value, Props) -> lists:keyreplace(Key, 1, Props, {Key, Value}). @@ -104,7 +104,7 @@ filter(Fun, Props) when is_function(Fun, 1), is_list(Props) -> [P || P <- Props, Fun(P)]. --spec filter_empty([{any(), any()} | atom()]) -> [{any(), any()} | atom()]. +-spec filter_empty(kz_term:proplist()) -> kz_term:proplist(). filter_empty(Props) -> filter(fun is_not_empty/1, Props). @@ -112,7 +112,7 @@ filter_empty(Props) -> is_not_empty({_, V}) -> not kz_term:is_empty(V); is_not_empty(_V) -> 'true'. --spec filter_empty_strings([{any(), any()} | atom()]) -> [{any(), any()} | atom()]. +-spec filter_empty_strings(kz_term:proplist()) -> kz_term:proplist(). filter_empty_strings(Props) -> filter(fun is_not_empty_string/1, Props). diff --git a/doc/mkdocs/mkdocs.yml b/doc/mkdocs/mkdocs.yml index 013ead0056a..2e61bea0c3e 100644 --- a/doc/mkdocs/mkdocs.yml +++ b/doc/mkdocs/mkdocs.yml @@ -364,6 +364,7 @@ nav: - 'applications/stepswitch/doc/resource_selectors.md' - 'applications/stepswitch/doc/rules.md' - 'applications/stepswitch/doc/maintenance.md' + - 'applications/stepswitch/doc/emergency.md' - 'Sysconf': - 'applications/sysconf/doc/README.md' - 'Tasks':