Improve zvonok verification call (#3768)

# What this PR does

Added support for message template during phone number verification for
the Zvonok service. Template message support allows the use of
[SSML](https://www.w3.org/TR/2010/REC-speech-synthesis11-20100907/)
markup depending on the speaker used for speech synthesis.

Example: `Your verification code is <prosody
rate="x-slow">$verification_code</prosody>`

## Which issue(s) this PR fixes

## Checklist

- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)

---------

Co-authored-by: Innokentii Konstantinov <innokenty.konstantinov@grafana.com>
Co-authored-by: Joey Orlando <joey.orlando@grafana.com>
Co-authored-by: Joey Orlando <joseph.t.orlando@gmail.com>
This commit is contained in:
Andrey Oleynik 2024-02-08 22:05:11 +03:00 committed by GitHub
parent cb48842e49
commit 7cd814507e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 83 additions and 3 deletions

View file

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- Improved zvonok verification call @sreway ([#3768](https://github.com/grafana/oncall/pull/3768))
### Changed
- Allow mobile app to access escalation options endpoints @imtoori ([#3847](https://github.com/grafana/oncall/pull/3847))

View file

@ -224,7 +224,9 @@ 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 process the call status, it is required to add a postback with the GET/POST method on the side of the zvonok.com
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.`.
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

@ -70,6 +70,7 @@ class LiveSetting(models.Model):
"ZVONOK_POSTBACK_STATUS",
"ZVONOK_POSTBACK_USER_CHOICE",
"ZVONOK_POSTBACK_USER_CHOICE_ACK",
"ZVONOK_VERIFICATION_TEMPLATE",
)
DESCRIPTIONS = {
@ -167,6 +168,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).",
}
SECRET_SETTING_NAMES = (

View file

@ -1,5 +1,6 @@
import logging
from random import randint
from string import Template
from typing import Optional
import requests
@ -104,15 +105,25 @@ class ZvonokPhoneProvider(PhoneProvider):
return f"Failed make call to {number}"
def make_verification_call(self, number: str):
code = str(randint(100000, 999999))
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
)
else:
message = f"Your verification code is {codewspaces}"
try:
response = self._call_create(number, f"Your verification code is {codewspaces}", speaker)
response = self._call_create(
number,
message,
speaker,
)
response.raise_for_status()
body = response.json()
if not body:
@ -139,6 +150,9 @@ class ZvonokPhoneProvider(PhoneProvider):
def _cache_key(self, number):
return f"zvonok_provider_{number}"
def _generate_verification_code(self):
return str(randint(100000, 999999))
@property
def flags(self) -> ProviderFlags:
return ProviderFlags(

View file

View file

@ -0,0 +1,57 @@
from unittest.mock import MagicMock, patch
import pytest
from django.test import override_settings
from apps.zvonok.phone_provider import ZvonokPhoneProvider
@pytest.fixture
def provider():
return ZvonokPhoneProvider()
@pytest.mark.django_db
def test_make_verification_call_with_template_set(provider):
verification_code = "123456"
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):
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)
@pytest.mark.django_db
def test_make_verification_call_with_invalid_template_set(provider):
verification_code = "123456"
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)
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"
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):
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)

View file

@ -842,6 +842,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)
DETACHED_INTEGRATIONS_SERVER = getenv_boolean("DETACHED_INTEGRATIONS_SERVER", default=False)