commit
b6b8bb2296
15 changed files with 689 additions and 92 deletions
|
|
@ -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}`
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
`,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
`,
|
||||
|
|
|
|||
198
tools/twilio/basic_flow.json
Normal file
198
tools/twilio/basic_flow.json
Normal 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
|
||||
}
|
||||
}
|
||||
386
tools/twilio/flow_with_routes.json
Normal file
386
tools/twilio/flow_with_routes.json
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue