change zvonok call verification (#4393)

# Change zvonok call verification

After May 27, the Zvonok service will block number verification that was
not set up as part of the phone number verification campaign. This PR
modifies the number verification process.

<!--
*Note*: if you have more than one GitHub issue that this PR closes, be
sure to preface
each issue link with a [closing
keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue).
This ensures that the issue(s) are auto-closed once the PR has been
merged.
-->

## 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] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
    show up in the autogenerated release notes.

Co-authored-by: Innokentii Konstantinov <innokenty.konstantinov@grafana.com>
This commit is contained in:
Andrey Oleynik 2024-06-04 08:34:57 +03:00 committed by GitHub
parent 8539514d85
commit d0dd15453e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 42 additions and 57 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

@ -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 = (

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

@ -912,7 +912,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)