Merge pull request #4460 from grafana/dev

dev to main
This commit is contained in:
Vadim Stepanov 2024-06-04 20:17:25 +01:00 committed by GitHub
commit b6b8bb2296
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 689 additions and 92 deletions

View file

@ -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}`

View file

@ -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):

View file

@ -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"])

View file

@ -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
]

View file

@ -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))

View file

@ -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 <prosody rate="x-slow">$verification_code</prosody>'
excepted_message = 'Your code is <prosody rate="x-slow">1 2 3 4 5 6</prosody>'
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)

View file

@ -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)

View file

@ -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 = <RT extends DefaultRecordType = DefaultRecordType>(props:
}, [rowSelection, columnsProp, data]);
return (
<div className={styles.root} data-testid="test__gTable">
<div className={cx(styles.root, { [styles.fixed]: props.tableLayout === 'fixed' })} data-testid="test__gTable">
<Table<RT>
expandable={expandable}
rowKey={rowKey}
@ -156,20 +155,24 @@ export const GTable = <RT extends DefaultRecordType = DefaultRecordType>(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;
`,
});

View file

@ -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<SchedulePersonalProps> = observer(({ userPk, onSlotC
<div className={styles.header}>
<HorizontalGroup justify="space-between">
<HorizontalGroup>
<Text type="secondary">
On-call schedule <Avatar src={storeUser.avatar} size="small" /> {storeUser.username}
</Text>
<RenderConditionally
shouldRender={Boolean(storeUser)}
render={() => (
<Text type="secondary">
On-call schedule <Avatar src={storeUser.avatar} size="small" /> {storeUser.username}
</Text>
)}
/>
{isOncall ? (
<Badge text="On-call now" color="green" />
) : (
/* @ts-ignore */
<Badge text="Not on-call now" color="gray" />
<Badge text="Not on-call now" color={'gray' as BadgeColor} />
)}
</HorizontalGroup>
<HorizontalGroup>

View file

@ -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,

View file

@ -598,23 +598,35 @@ class _SchedulePage extends React.Component<SchedulePageProps, SchedulePageState
history.replace(`${PLUGIN_ROOT}/schedules`);
};
handleShowShiftSwapForm = (id: ShiftSwap['id'] | 'new') => {
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)

View file

@ -115,6 +115,7 @@ class _SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSt
total: results ? Math.ceil((count || 0) / page_size) : 0,
onChange: this.handlePageChange,
}}
tableLayout="fixed"
rowKey="id"
expandable={{
expandedRowKeys: expandedRowKeys,

View file

@ -19,7 +19,7 @@ export const getUtilStyles = (theme: GrafanaTheme2) => {
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%;
`,

View file

@ -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": "<YOUR_INTEGRATION_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": "<YOUR_INTEGRATION_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
}
}

View file

@ -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": "<YOUR_INTEGRATION_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": "<YOUR_INTEGRATION_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
}
}