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)