diff --git a/docs/sources/set-up/open-source/index.md b/docs/sources/set-up/open-source/index.md index b925c7b3..734ec8fc 100644 --- a/docs/sources/set-up/open-source/index.md +++ b/docs/sources/set-up/open-source/index.md @@ -234,8 +234,7 @@ Zvonok.com, complete the following steps: to the variable `ZVONOK_AUDIO_ID` (optional step). 6. To make a call with a specific voice, you can set the `ZVONOK_SPEAKER_ID`. By default, the ID used is `Salli` (optional step). -7. To change the voice message for phone verification, you can set the variable `ZVONOK_VERIFICATION_TEMPLATE` - with the following format (optional step): `Your verification code is $verification_code, have a nice day.`. +7. Create phone number verification campaign with type `tellcode` and assign its ID value to `ZVONOK_VERIFICATION_CAMPAIGN_ID`. 8. To process the call status, it is required to add a postback with the GET/POST method on the side of the zvonok.com service with the following format (optional step): `${ONCALL_BASE_URL}/zvonok/call_status_events?campaign_id={ct_campaign_id}&call_id={ct_call_id}&status={ct_status}&user_choice={ct_user_choice}` diff --git a/engine/apps/api/views/alert_group.py b/engine/apps/api/views/alert_group.py index f47f3ccf..96d49ade 100644 --- a/engine/apps/api/views/alert_group.py +++ b/engine/apps/api/views/alert_group.py @@ -1,5 +1,6 @@ import typing +from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.db.models import Count, Max, Q from django_filters import rest_framework as filters @@ -275,6 +276,11 @@ class AlertGroupView( pagination_class = AlertGroupCursorPaginator filter_backends = [SearchFilter, filters.DjangoFilterBackend] + search_fields = ( + ["=public_primary_key", "=inside_organization_number", "web_title_cache"] + if settings.FEATURE_ALERT_GROUP_SEARCH_ENABLED + else [] + ) filterset_class = AlertGroupFilter def get_serializer_class(self): diff --git a/engine/apps/base/models/live_setting.py b/engine/apps/base/models/live_setting.py index f6e8b7e5..e4c4a2ab 100644 --- a/engine/apps/base/models/live_setting.py +++ b/engine/apps/base/models/live_setting.py @@ -71,7 +71,7 @@ class LiveSetting(models.Model): "ZVONOK_POSTBACK_STATUS", "ZVONOK_POSTBACK_USER_CHOICE", "ZVONOK_POSTBACK_USER_CHOICE_ACK", - "ZVONOK_VERIFICATION_TEMPLATE", + "ZVONOK_VERIFICATION_CAMPAIGN_ID", ) DESCRIPTIONS = { @@ -170,7 +170,7 @@ class LiveSetting(models.Model): "ZVONOK_POSTBACK_STATUS": "'Postback' status (ct_status) query parameter name to validate a postback request.", "ZVONOK_POSTBACK_USER_CHOICE": "'Postback' user choice (ct_user_choice) query parameter name (optional).", "ZVONOK_POSTBACK_USER_CHOICE_ACK": "'Postback' user choice (ct_user_choice) query parameter value for acknowledge alert group (optional).", - "ZVONOK_VERIFICATION_TEMPLATE": "The message template used for phone number verification (optional).", + "ZVONOK_VERIFICATION_CAMPAIGN_ID": "The phone number verification campaign ID. You can get it after verification campaign creation.", } SECRET_SETTING_NAMES = ( @@ -240,7 +240,7 @@ class LiveSetting(models.Model): @classmethod def validate_settings(cls): - settings_to_validate = cls.objects.all() + settings_to_validate = cls.objects.filter(name__in=cls.AVAILABLE_NAMES) for setting in settings_to_validate: setting.error = LiveSettingValidator(live_setting=setting).get_error() setting.save(update_fields=["error"]) diff --git a/engine/apps/slack/scenarios/paging.py b/engine/apps/slack/scenarios/paging.py index ecce5b5d..22532dc0 100644 --- a/engine/apps/slack/scenarios/paging.py +++ b/engine/apps/slack/scenarios/paging.py @@ -681,7 +681,7 @@ def _create_user_option_groups( "text": f"{user.name or user.username}", "emoji": True, }, - "value": make_value({"id": user.pk}, organization), + "value": json.dumps({"id": user.pk}), } for user in users ] diff --git a/engine/apps/zvonok/phone_provider.py b/engine/apps/zvonok/phone_provider.py index e19e771f..4af75b6b 100644 --- a/engine/apps/zvonok/phone_provider.py +++ b/engine/apps/zvonok/phone_provider.py @@ -1,6 +1,5 @@ import logging from random import randint -from string import Template from typing import Optional import requests @@ -12,6 +11,7 @@ from apps.phone_notifications.phone_provider import PhoneProvider, ProviderFlags from apps.zvonok.models.phone_call import ZvonokCallStatuses, ZvonokPhoneCall ZVONOK_CALL_URL = "https://zvonok.com/manager/cabapi_external/api/v1/phones/call/" +ZVONOK_VERIFICATION_CALL_URL = "https://zvonok.com/manager/cabapi_external/api/v1/phones/tellcode/" logger = logging.getLogger(__name__) @@ -96,6 +96,15 @@ class ZvonokPhoneProvider(PhoneProvider): return requests.post(ZVONOK_CALL_URL, params=params) + def _verification_call_create(self, number: str, code: int): + params = { + "public_key": live_settings.ZVONOK_API_KEY, + "campaign_id": live_settings.ZVONOK_VERIFICATION_CAMPAIGN_ID, + "phone": number, + "pincode": code, + } + return requests.post(ZVONOK_VERIFICATION_CALL_URL, params=params) + def _get_graceful_msg(self, body, number): if body: status = body.get("status") @@ -105,34 +114,19 @@ class ZvonokPhoneProvider(PhoneProvider): return f"Failed make call to {number}" def make_verification_call(self, number: str): + body = None code = self._generate_verification_code() cache.set(self._cache_key(number), code, timeout=10 * 60) - codewspaces = " ".join(code) - body = None - speaker = live_settings.ZVONOK_SPEAKER_ID - - if live_settings.ZVONOK_VERIFICATION_TEMPLATE: - message = Template(live_settings.ZVONOK_VERIFICATION_TEMPLATE).safe_substitute( - verification_code=codewspaces + if not live_settings.ZVONOK_VERIFICATION_CAMPAIGN_ID: + raise FailedToStartVerification( + graceful_msg="Failed make verification call, verification campaign id not set." ) - else: - message = f"Your verification code is {codewspaces}" + try: - response = self._call_create( - number, - message, - speaker, - ) - response.raise_for_status() + response = self._verification_call_create(number, code) body = response.json() - if not body: - logger.error("ZvonokPhoneProvider.make_verification_call: failed, empty body") - raise FailedToMakeCall(graceful_msg=f"Failed make verification call to {number}, empty body") - - call_id = body.get("call_id") - if not call_id: - raise FailedToStartVerification(graceful_msg=self._get_graceful_msg(body, number)) + response.raise_for_status() except requests.exceptions.HTTPError as http_err: logger.error(f"ZvonokPhoneProvider.make_verification_call: failed {http_err}") raise FailedToStartVerification(graceful_msg=self._get_graceful_msg(body, number)) diff --git a/engine/apps/zvonok/tests/test_zvonok_provider.py b/engine/apps/zvonok/tests/test_zvonok_provider.py index 04eab9da..5e25045f 100644 --- a/engine/apps/zvonok/tests/test_zvonok_provider.py +++ b/engine/apps/zvonok/tests/test_zvonok_provider.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch import pytest from django.test import override_settings +from apps.phone_notifications.exceptions import FailedToStartVerification from apps.zvonok.phone_provider import ZvonokPhoneProvider @@ -12,46 +13,37 @@ def provider(): @pytest.mark.django_db -def test_make_verification_call_with_template_set(provider): - verification_code = "123456" +def test_make_verification_call(provider): + verification_code = "123456789" number = "1234567890" - speaker_id = "Salli" - template_value = 'Your code is $verification_code' - excepted_message = 'Your code is 1 2 3 4 5 6' - - with override_settings(ZVONOK_VERIFICATION_TEMPLATE=template_value, ZVONOK_SPEAKER_ID=speaker_id): + campaign_id = "123456" + with override_settings(ZVONOK_VERIFICATION_CAMPAIGN_ID=campaign_id): with patch("django.core.cache.cache.set"): - provider._call_create = MagicMock(return_value=MagicMock(json=lambda: {"call_id": "12345"})) + provider._verification_call_create = MagicMock(return_value=MagicMock(json=lambda: {"status": "ok"})) provider._generate_verification_code = MagicMock(return_value=verification_code) provider.make_verification_call(number) - provider._call_create.assert_called_once_with(number, excepted_message, speaker_id) + provider._verification_call_create.assert_called_once_with(number, verification_code) @pytest.mark.django_db -def test_make_verification_call_with_invalid_template_set(provider): - verification_code = "123456" +def test_make_verification_call_without_campaign_id(provider): number = "1234567890" - speaker_id = "Salli" - template_value = "Your code is" - excepted_message = "Your code is" - - with override_settings(ZVONOK_VERIFICATION_TEMPLATE=template_value, ZVONOK_SPEAKER_ID=speaker_id): - with patch("django.core.cache.cache.set"): - provider._call_create = MagicMock(return_value=MagicMock(json=lambda: {"call_id": "12345"})) - provider._generate_verification_code = MagicMock(return_value=verification_code) + with patch("django.core.cache.cache.set"): + with pytest.raises(FailedToStartVerification): provider.make_verification_call(number) - provider._call_create.assert_called_once_with(number, excepted_message, speaker_id) @pytest.mark.django_db -def test_make_verification_call_without_template_set(provider): - verification_code = "123456" +def test_make_verification_call_with_error(provider): number = "1234567890" - speaker_id = "Salli" - excepted_message = "Your verification code is 1 2 3 4 5 6" - with override_settings(ZVONOK_SPEAKER_ID=speaker_id): + campaign_id = "123456" + + with override_settings(ZVONOK_VERIFICATION_CAMPAIGN_ID=campaign_id): with patch("django.core.cache.cache.set"): - provider._call_create = MagicMock(return_value=MagicMock(json=lambda: {"call_id": "12345"})) - provider._generate_verification_code = MagicMock(return_value=verification_code) - provider.make_verification_call(number) - provider._call_create.assert_called_once_with(number, excepted_message, speaker_id) + with pytest.raises(FailedToStartVerification): + provider._verification_call_create = MagicMock( + return_value=MagicMock( + json={"status": "error", "data": "Form isn't valid: * campaign_id\n * Invalid campaign type"} + ) + ) + provider.make_verification_call(number) diff --git a/engine/settings/base.py b/engine/settings/base.py index 48d3fbdd..eef9d16e 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -71,6 +71,7 @@ GRAFANA_CLOUD_NOTIFICATIONS_ENABLED = getenv_boolean("GRAFANA_CLOUD_NOTIFICATION FEATURE_LABELS_ENABLED_FOR_ALL = getenv_boolean("FEATURE_LABELS_ENABLED_FOR_ALL", default=False) # Enable labels feature for organizations from the list. Use OnCall organization ID, for this flag FEATURE_LABELS_ENABLED_PER_ORG = getenv_list("FEATURE_LABELS_ENABLED_PER_ORG", default=list()) +FEATURE_ALERT_GROUP_SEARCH_ENABLED = getenv_boolean("FEATURE_ALERT_GROUP_SEARCH_ENABLED", default=False) TWILIO_API_KEY_SID = os.environ.get("TWILIO_API_KEY_SID") TWILIO_API_KEY_SECRET = os.environ.get("TWILIO_API_KEY_SECRET") @@ -912,7 +913,7 @@ ZVONOK_POSTBACK_CAMPAIGN_ID = os.getenv("ZVONOK_POSTBACK_CAMPAIGN_ID", "campaign ZVONOK_POSTBACK_STATUS = os.getenv("ZVONOK_POSTBACK_STATUS", "status") ZVONOK_POSTBACK_USER_CHOICE = os.getenv("ZVONOK_POSTBACK_USER_CHOICE", None) ZVONOK_POSTBACK_USER_CHOICE_ACK = os.getenv("ZVONOK_POSTBACK_USER_CHOICE_ACK", None) -ZVONOK_VERIFICATION_TEMPLATE = os.getenv("ZVONOK_VERIFICATION_TEMPLATE", None) +ZVONOK_VERIFICATION_CAMPAIGN_ID = os.getenv("ZVONOK_VERIFICATION_CAMPAIGN_ID", None) DETACHED_INTEGRATIONS_SERVER = getenv_boolean("DETACHED_INTEGRATIONS_SERVER", default=False) diff --git a/grafana-plugin/src/components/GTable/GTable.tsx b/grafana-plugin/src/components/GTable/GTable.tsx index 82444645..b251a4ee 100644 --- a/grafana-plugin/src/components/GTable/GTable.tsx +++ b/grafana-plugin/src/components/GTable/GTable.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useMemo, ChangeEvent, ReactElement } from 'react'; import { css, cx } from '@emotion/css'; -import { GrafanaTheme2 } from '@grafana/data'; import { Pagination, Checkbox, Icon, useStyles2 } from '@grafana/ui'; import Table from 'rc-table'; import { TableProps } from 'rc-table/lib/Table'; @@ -136,7 +135,7 @@ export const GTable = (props: }, [rowSelection, columnsProp, data]); return ( -
+
expandable={expandable} rowKey={rowKey} @@ -156,20 +155,24 @@ export const GTable = (props: ); }; -const getGTableStyles = (_theme: GrafanaTheme2) => { - return { - root: css` - table { - width: 100%; - } - `, +const getGTableStyles = () => ({ + root: css` + table { + width: 100%; + } + `, - pagination: css` - margin-top: 20px; - `, + fixed: css` + table { + table-layout: fixed; + } + `, - checkbox: css` - display: inline-flex; - `, - }; -}; + pagination: css` + margin-top: 20px; + `, + + checkbox: css` + display: inline-flex; + `, +}); diff --git a/grafana-plugin/src/containers/Rotations/SchedulePersonal.tsx b/grafana-plugin/src/containers/Rotations/SchedulePersonal.tsx index 9adb92d7..57145fc2 100644 --- a/grafana-plugin/src/containers/Rotations/SchedulePersonal.tsx +++ b/grafana-plugin/src/containers/Rotations/SchedulePersonal.tsx @@ -2,12 +2,13 @@ import React, { FC, useEffect } from 'react'; import { cx } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { Badge, Button, HorizontalGroup, Icon, useStyles2, withTheme2 } from '@grafana/ui'; +import { Badge, BadgeColor, Button, HorizontalGroup, Icon, useStyles2, withTheme2 } from '@grafana/ui'; import { observer } from 'mobx-react'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; import { Avatar } from 'components/Avatar/Avatar'; +import { RenderConditionally } from 'components/RenderConditionally/RenderConditionally'; import { Text } from 'components/Text/Text'; import { Rotation } from 'containers/Rotation/Rotation'; import { TimelineMarks } from 'containers/TimelineMarks/TimelineMarks'; @@ -102,14 +103,18 @@ const _SchedulePersonal: FC = observer(({ userPk, onSlotC
- - On-call schedule {storeUser.username} - + ( + + On-call schedule {storeUser.username} + + )} + /> {isOncall ? ( ) : ( - /* @ts-ignore */ - + )} diff --git a/grafana-plugin/src/models/alertgroup/alertgroup.ts b/grafana-plugin/src/models/alertgroup/alertgroup.ts index 8241213a..f3c93972 100644 --- a/grafana-plugin/src/models/alertgroup/alertgroup.ts +++ b/grafana-plugin/src/models/alertgroup/alertgroup.ts @@ -65,7 +65,7 @@ export class AlertGroupStore { params: { query: { ...incidentFilters, - search, + search: incidentFilters?.search || search, perpage: this.alertsSearchResult?.page_size, cursor: this.incidentsCursor, is_root: true, diff --git a/grafana-plugin/src/pages/schedule/Schedule.tsx b/grafana-plugin/src/pages/schedule/Schedule.tsx index 703627ad..80fb1bdb 100644 --- a/grafana-plugin/src/pages/schedule/Schedule.tsx +++ b/grafana-plugin/src/pages/schedule/Schedule.tsx @@ -598,23 +598,35 @@ class _SchedulePage extends React.Component { + handleShowShiftSwapForm = (id: ShiftSwap['id'] | 'new', swap?: { swap_start: string; swap_end: string }) => { + const { filters } = this.state; const { store, + store: { + userStore: { currentUserPk }, + timezoneStore: { currentDateInSelectedTimezone }, + }, match: { params: { id: scheduleId }, }, } = this.props; - const { - userStore: { currentUserPk }, - timezoneStore: { currentDateInSelectedTimezone }, - } = store; + if (swap) { + if (!filters.users.includes(currentUserPk)) { + this.setState({ filters: { ...filters, users: [...this.state.filters.users, currentUserPk] } }); + this.highlightMyShiftsWasToggled = true; + } + + return this.setState({ + shiftSwapIdToShowForm: id, + shiftSwapParamsToShowForm: { + swap_start: swap.swap_start, + swap_end: swap.swap_end, + }, + }); + } const layers = getLayersFromStore(store, scheduleId, store.timezoneStore.calendarStartDate); - - const { filters } = this.state; - const closestEvent = findClosestUserEvent(dayjs(), currentUserPk, layers); const swapStart = closestEvent ? dayjs(closestEvent.start) diff --git a/grafana-plugin/src/pages/schedules/Schedules.tsx b/grafana-plugin/src/pages/schedules/Schedules.tsx index 7a16317d..de49ee6d 100644 --- a/grafana-plugin/src/pages/schedules/Schedules.tsx +++ b/grafana-plugin/src/pages/schedules/Schedules.tsx @@ -115,6 +115,7 @@ class _SchedulesPage extends React.Component { thinLineBreak: css` width: 100%; - border-top: 1px solid ${theme.colors.secondary.main}; + border-top: 1px solid ${theme.colors.secondary.contrastText}; margin-top: 8px; opacity: 15%; `, diff --git a/tools/twilio/basic_flow.json b/tools/twilio/basic_flow.json new file mode 100644 index 00000000..b168d8e2 --- /dev/null +++ b/tools/twilio/basic_flow.json @@ -0,0 +1,198 @@ +{ + "description": "Basic SMS and Call escalation", + "states": [ + { + "name": "Trigger", + "type": "trigger", + "transitions": [ + { + "next": "send_alert_from_sms", + "event": "incomingMessage" + }, + { + "next": "describe_alert_from_call", + "event": "incomingCall" + }, + { + "event": "incomingConversationMessage" + }, + { + "event": "incomingRequest" + }, + { + "event": "incomingParent" + } + ], + "properties": { + "offset": { + "x": 0, + "y": 0 + } + } + }, + { + "name": "send_alert_from_sms", + "type": "make-http-request", + "transitions": [ + { + "next": "send_alert_from_sms_success", + "event": "success" + }, + { + "next": "send_alert_from_sms_fail", + "event": "failed" + } + ], + "properties": { + "offset": { + "x": -180, + "y": 250 + }, + "method": "POST", + "content_type": "application/json;charset=utf-8", + "body": "{\"from\":\"{{trigger.message.From}}\",\"message\":\"{{trigger.message.Body}}\"}", + "url": "" + } + }, + { + "name": "send_alert_from_sms_success", + "type": "send-message", + "transitions": [ + { + "event": "sent" + }, + { + "event": "failed" + } + ], + "properties": { + "offset": { + "x": -410, + "y": 590 + }, + "service": "{{trigger.message.InstanceSid}}", + "channel": "{{trigger.message.ChannelSid}}", + "from": "{{flow.channel.address}}", + "message_type": "custom", + "to": "{{contact.channel.address}}", + "body": "Alert sent successfully" + } + }, + { + "name": "send_alert_from_sms_fail", + "type": "send-message", + "transitions": [ + { + "event": "sent" + }, + { + "event": "failed" + } + ], + "properties": { + "offset": { + "x": -60, + "y": 590 + }, + "service": "{{trigger.message.InstanceSid}}", + "channel": "{{trigger.message.ChannelSid}}", + "from": "{{flow.channel.address}}", + "message_type": "custom", + "to": "{{contact.channel.address}}", + "body": "Failed to send alert: Status({{widgets.send_escalation.status_code}})" + } + }, + { + "name": "describe_alert_from_call", + "type": "gather-input-on-call", + "transitions": [ + { + "event": "keypress" + }, + { + "next": "send_alert_from_call", + "event": "speech" + }, + { + "event": "timeout" + } + ], + "properties": { + "speech_timeout": "auto", + "offset": { + "x": 350, + "y": 240 + }, + "loop": 1, + "finish_on_key": "#", + "say": "Describe the alert to send. Press pound when finished.", + "stop_gather": true, + "gather_language": "en", + "profanity_filter": "true", + "timeout": 60 + } + }, + { + "name": "send_alert_from_call", + "type": "make-http-request", + "transitions": [ + { + "next": "send_alert_from_call_success", + "event": "success" + }, + { + "next": "send_alert_from_call_fail", + "event": "failed" + } + ], + "properties": { + "offset": { + "x": 360, + "y": 590 + }, + "method": "POST", + "content_type": "application/json;charset=utf-8", + "body": "{\"from\":\"{{trigger.call.From}}\", \"message\":\"{{widgets.describe_alert_from_call.SpeechResult}} \"}", + "url": "" + } + }, + { + "name": "send_alert_from_call_success", + "type": "say-play", + "transitions": [ + { + "event": "audioComplete" + } + ], + "properties": { + "offset": { + "x": 90, + "y": 900 + }, + "loop": 1, + "say": "Alert sent successfully" + } + }, + { + "name": "send_alert_from_call_fail", + "type": "say-play", + "transitions": [ + { + "event": "audioComplete" + } + ], + "properties": { + "offset": { + "x": 520, + "y": 900 + }, + "loop": 1, + "say": "Failed to send alert: Status ({{widgets.send_alert_from_call.status_code}})" + } + } + ], + "initial_state": "Trigger", + "flags": { + "allow_concurrent_calls": true + } +} diff --git a/tools/twilio/flow_with_routes.json b/tools/twilio/flow_with_routes.json new file mode 100644 index 00000000..add3add4 --- /dev/null +++ b/tools/twilio/flow_with_routes.json @@ -0,0 +1,386 @@ +{ + "description": "Added Routes SMS and Call escalation", + "states": [ + { + "name": "Trigger", + "type": "trigger", + "transitions": [ + { + "next": "sms_select_target", + "event": "incomingMessage" + }, + { + "next": "call_select_target", + "event": "incomingCall" + }, + { + "event": "incomingConversationMessage" + }, + { + "event": "incomingRequest" + }, + { + "event": "incomingParent" + } + ], + "properties": { + "offset": { + "x": 80, + "y": -200 + } + } + }, + { + "name": "send_alert_from_sms", + "type": "make-http-request", + "transitions": [ + { + "next": "send_alert_from_sms_success", + "event": "success" + }, + { + "next": "send_alert_from_sms_fail", + "event": "failed" + } + ], + "properties": { + "offset": { + "x": -370, + "y": 500 + }, + "method": "POST", + "content_type": "application/json;charset=utf-8", + "body": "{\"from\":\"{{trigger.message.From}}\",\"message\":\"{{trigger.message.Body}}\",\"target\":\"{{widgets.sms_select_target.inbound.Body}}\"}", + "url": "" + } + }, + { + "name": "send_alert_from_sms_success", + "type": "send-message", + "transitions": [ + { + "event": "sent" + }, + { + "event": "failed" + } + ], + "properties": { + "offset": { + "x": -700, + "y": 780 + }, + "service": "{{trigger.message.InstanceSid}}", + "channel": "{{trigger.message.ChannelSid}}", + "from": "{{flow.channel.address}}", + "message_type": "custom", + "to": "{{contact.channel.address}}", + "body": "Alert sent successfully" + } + }, + { + "name": "send_alert_from_sms_fail", + "type": "send-message", + "transitions": [ + { + "event": "sent" + }, + { + "event": "failed" + } + ], + "properties": { + "offset": { + "x": -340, + "y": 780 + }, + "service": "{{trigger.message.InstanceSid}}", + "channel": "{{trigger.message.ChannelSid}}", + "from": "{{flow.channel.address}}", + "message_type": "custom", + "to": "{{contact.channel.address}}", + "body": "Failed to send alert: Status({{widgets.send_escalation.status_code}})" + } + }, + { + "name": "describe_alert_from_call", + "type": "gather-input-on-call", + "transitions": [ + { + "event": "keypress" + }, + { + "next": "send_alert_from_call", + "event": "speech" + }, + { + "event": "timeout" + } + ], + "properties": { + "speech_timeout": "auto", + "offset": { + "x": 350, + "y": 310 + }, + "loop": 1, + "finish_on_key": "#", + "say": "Describe the alert to send. Press pound when finished.", + "stop_gather": true, + "gather_language": "en", + "profanity_filter": "true", + "timeout": 60 + } + }, + { + "name": "send_alert_from_call", + "type": "make-http-request", + "transitions": [ + { + "next": "send_alert_from_call_success", + "event": "success" + }, + { + "next": "send_alert_from_call_fail", + "event": "failed" + } + ], + "properties": { + "offset": { + "x": 350, + "y": 580 + }, + "method": "POST", + "content_type": "application/json;charset=utf-8", + "body": "{\"from\":\"{{trigger.call.From}}\", \"message\":\"{{widgets.describe_alert_from_call.SpeechResult}} \",\"target\":\"{{widgets.call_set_target.target}}\"}", + "url": "" + } + }, + { + "name": "send_alert_from_call_success", + "type": "say-play", + "transitions": [ + { + "event": "audioComplete" + } + ], + "properties": { + "offset": { + "x": 200, + "y": 950 + }, + "loop": 1, + "say": "Alert sent successfully" + } + }, + { + "name": "send_alert_from_call_fail", + "type": "say-play", + "transitions": [ + { + "event": "audioComplete" + } + ], + "properties": { + "offset": { + "x": 630, + "y": 950 + }, + "loop": 1, + "say": "Failed to send alert: Status ({{widgets.send_alert_from_call.status_code}})" + } + }, + { + "name": "sms_select_target", + "type": "send-and-wait-for-reply", + "transitions": [ + { + "next": "sms_validate_target", + "event": "incomingMessage" + }, + { + "next": "sms_select_target_timeout", + "event": "timeout" + }, + { + "event": "deliveryFailure" + } + ], + "properties": { + "offset": { + "x": -330, + "y": -50 + }, + "service": "{{trigger.message.InstanceSid}}", + "channel": "{{trigger.message.ChannelSid}}", + "from": "{{flow.channel.address}}", + "message_type": "custom", + "body": "Which target do you want to send the alert to?\nabc \ndefault", + "timeout": "300" + } + }, + { + "name": "sms_select_target_timeout", + "type": "send-message", + "transitions": [ + { + "event": "sent" + }, + { + "event": "failed" + } + ], + "properties": { + "offset": { + "x": -80, + "y": 210 + }, + "service": "{{trigger.message.InstanceSid}}", + "channel": "{{trigger.message.ChannelSid}}", + "from": "{{flow.channel.address}}", + "message_type": "custom", + "to": "{{contact.channel.address}}", + "body": "Target select timed out, send the alert again to start over." + } + }, + { + "name": "sms_validate_target", + "type": "split-based-on", + "transitions": [ + { + "next": "sms_validate_target_fail", + "event": "noMatch" + }, + { + "next": "send_alert_from_sms", + "event": "match", + "conditions": [ + { + "friendly_name": "If value equal_to abc", + "arguments": ["{{widgets.sms_select_target.inbound.Body}}"], + "type": "matches_any_of", + "value": "abc,default" + } + ] + } + ], + "properties": { + "input": "{{widgets.sms_select_target.inbound.Body}}", + "offset": { + "x": -590, + "y": 210 + } + } + }, + { + "name": "sms_validate_target_fail", + "type": "send-message", + "transitions": [ + { + "event": "sent" + }, + { + "event": "failed" + } + ], + "properties": { + "offset": { + "x": -700, + "y": 500 + }, + "service": "{{trigger.message.InstanceSid}}", + "channel": "{{trigger.message.ChannelSid}}", + "from": "{{flow.channel.address}}", + "message_type": "custom", + "to": "{{contact.channel.address}}", + "body": "{{widgets.sms_select_target.inbound.Body}} is not a valid target." + } + }, + { + "name": "call_select_target", + "type": "gather-input-on-call", + "transitions": [ + { + "next": "call_select_validate", + "event": "keypress" + }, + { + "event": "speech" + }, + { + "event": "timeout" + } + ], + "properties": { + "number_of_digits": 1, + "speech_timeout": "auto", + "offset": { + "x": 350, + "y": 50 + }, + "loop": 1, + "finish_on_key": "#", + "say": "Which target do you want to send to? Press 1 for ABC. \nPress 2 for default.", + "stop_gather": true, + "gather_language": "en", + "profanity_filter": "true", + "timeout": 5 + } + }, + { + "name": "call_select_validate", + "type": "split-based-on", + "transitions": [ + { + "next": "call_select_target", + "event": "noMatch" + }, + { + "next": "call_set_target", + "event": "match", + "conditions": [ + { + "friendly_name": "If value matches_any_of 1,2", + "arguments": ["{{widgets.call_select_target.Digits}}"], + "type": "matches_any_of", + "value": "1,2" + } + ] + } + ], + "properties": { + "input": "{{widgets.call_select_target.Digits}}", + "offset": { + "x": 760, + "y": 50 + } + } + }, + { + "name": "call_set_target", + "type": "set-variables", + "transitions": [ + { + "next": "describe_alert_from_call", + "event": "next" + } + ], + "properties": { + "variables": [ + { + "value": "{% if widgets.call_select_target.Digits == \"1\" %}abc{% elsif widgets.call_select_target.Digits == \"2\" %}default{% endif %}", + "key": "target" + } + ], + "offset": { + "x": 760, + "y": 300 + } + } + } + ], + "initial_state": "Trigger", + "flags": { + "allow_concurrent_calls": true + } +}