From 7cd814507ed28bf07d0cacc811bdb518cc2f9ebc Mon Sep 17 00:00:00 2001 From: Andrey Oleynik Date: Thu, 8 Feb 2024 22:05:11 +0300 Subject: [PATCH] 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 $verification_code` ## 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 Co-authored-by: Joey Orlando Co-authored-by: Joey Orlando --- CHANGELOG.md | 4 ++ docs/sources/open-source/_index.md | 4 +- engine/apps/base/models/live_setting.py | 2 + engine/apps/zvonok/phone_provider.py | 18 +++++- engine/apps/zvonok/tests/__init__.py | 0 .../apps/zvonok/tests/test_zvonok_provider.py | 57 +++++++++++++++++++ engine/settings/base.py | 1 + 7 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 engine/apps/zvonok/tests/__init__.py create mode 100644 engine/apps/zvonok/tests/test_zvonok_provider.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e8768699..df0ca37b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/docs/sources/open-source/_index.md b/docs/sources/open-source/_index.md index 6491cc7b..d25c35b8 100644 --- a/docs/sources/open-source/_index.md +++ b/docs/sources/open-source/_index.md @@ -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}` diff --git a/engine/apps/base/models/live_setting.py b/engine/apps/base/models/live_setting.py index 324bc762..48c3ac06 100644 --- a/engine/apps/base/models/live_setting.py +++ b/engine/apps/base/models/live_setting.py @@ -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 = ( diff --git a/engine/apps/zvonok/phone_provider.py b/engine/apps/zvonok/phone_provider.py index 49d7c67b..e19e771f 100644 --- a/engine/apps/zvonok/phone_provider.py +++ b/engine/apps/zvonok/phone_provider.py @@ -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( diff --git a/engine/apps/zvonok/tests/__init__.py b/engine/apps/zvonok/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/engine/apps/zvonok/tests/test_zvonok_provider.py b/engine/apps/zvonok/tests/test_zvonok_provider.py new file mode 100644 index 00000000..04eab9da --- /dev/null +++ b/engine/apps/zvonok/tests/test_zvonok_provider.py @@ -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 $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): + 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) diff --git a/engine/settings/base.py b/engine/settings/base.py index 1c5b930a..f3a96eb6 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -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)