-
Notifications
You must be signed in to change notification settings - Fork 299
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added integration with [zvonok.com](https://zvonok.com) service. Features: - Phone number validation - Test calls - Selection of pre-recorded audio - Making calls - Processing call status - Acknowledgment alert group (optional) To process the call status, it is required to add a postback with the GET method on the side of the zvonok.com service with the following format ([more info here](https://zvonok.com/ru-ru/guide/guide_postback/)): ```${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}``` The names of the transmitted parameters can be redefined through environment variables. --------- Co-authored-by: Innokentii Konstantinov <innokenty.konstantinov@grafana.com>
- Loading branch information
1 parent
5cc9d54
commit aeb3500
Showing
14 changed files
with
461 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# Generated by Django 3.2.19 on 2023-07-01 12:28 | ||
|
||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
initial = True | ||
|
||
dependencies = [ | ||
('phone_notifications', '0001_initial'), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='ZvonokPhoneCall', | ||
fields=[ | ||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('created_at', models.DateTimeField(auto_now_add=True)), | ||
('status', models.PositiveSmallIntegerField(blank=True, choices=[(10, 'attempts_exc'), (20, 'compl_finished'), (30, 'compl_nofinished'), (40, 'deleted'), (50, 'duration_error'), (60, 'expires'), (70, 'novalid_button'), (80, 'no_provider'), (90, 'interrupted'), (100, 'in_process'), (110, 'pincode_nook'), (130, 'synth_error'), (140, 'user')], null=True)), | ||
('call_id', models.CharField(blank=True, max_length=50)), | ||
('campaign_id', models.CharField(max_length=50)), | ||
('phone_call_record', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='zvonok_zvonokphonecall_related', related_query_name='zvonok_zvonokphonecalls', to='phone_notifications.phonecallrecord')), | ||
], | ||
options={ | ||
'abstract': False, | ||
}, | ||
), | ||
] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .phone_call import ZvonokCallStatuses, ZvonokPhoneCall # noqa: F401 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
from django.db import models | ||
|
||
from apps.phone_notifications.phone_provider import ProviderPhoneCall | ||
|
||
|
||
class ZvonokCallStatuses: | ||
""" | ||
https://zvonok.com/ru-ru/guide/guide_statuses/ | ||
""" | ||
|
||
ATTEMPTS_EXC = 10 | ||
COMPL_FINISHED = 20 | ||
COMPL_NOFINISHED = 30 | ||
DELETED = 40 | ||
DURATION_ERROR = 50 | ||
EXPIRES = 60 | ||
NOVALID_BUTTON = 70 | ||
NO_PROVIDER = 80 | ||
INTERRUPTED = 90 | ||
IN_PROCESS = 100 | ||
PINCODE_NOOK = 110 | ||
PINCODE_OK = 120 | ||
SYNTH_ERROR = 130 | ||
USER = 140 | ||
|
||
CHOICES = ( | ||
(ATTEMPTS_EXC, "attempts_exc"), | ||
(COMPL_FINISHED, "compl_finished"), | ||
(COMPL_NOFINISHED, "compl_nofinished"), | ||
(DELETED, "deleted"), | ||
(DURATION_ERROR, "duration_error"), | ||
(EXPIRES, "expires"), | ||
(NOVALID_BUTTON, "novalid_button"), | ||
(NO_PROVIDER, "no_provider"), | ||
(INTERRUPTED, "interrupted"), | ||
(IN_PROCESS, "in_process"), | ||
(PINCODE_NOOK, "pincode_nook"), | ||
(SYNTH_ERROR, "synth_error"), | ||
(USER, "user"), | ||
) | ||
|
||
DETERMINANT = { | ||
"attempts_exc": ATTEMPTS_EXC, | ||
"compl_finished": COMPL_FINISHED, | ||
"deleted": DELETED, | ||
"duration_error": DURATION_ERROR, | ||
"expires": EXPIRES, | ||
"novalid_button": NOVALID_BUTTON, | ||
"no_provider": NO_PROVIDER, | ||
"interrupted": INTERRUPTED, | ||
"in_process": IN_PROCESS, | ||
"pincode_nook": PINCODE_NOOK, | ||
"synth_error": SYNTH_ERROR, | ||
"user": USER, | ||
} | ||
|
||
|
||
class ZvonokPhoneCall(ProviderPhoneCall, models.Model): | ||
created_at = models.DateTimeField(auto_now_add=True) | ||
|
||
status = models.PositiveSmallIntegerField( | ||
blank=True, | ||
null=True, | ||
choices=ZvonokCallStatuses.CHOICES, | ||
) | ||
|
||
call_id = models.CharField( | ||
blank=True, | ||
max_length=50, | ||
) | ||
|
||
campaign_id = models.CharField( | ||
max_length=50, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import logging | ||
from random import randint | ||
from typing import Optional | ||
|
||
import requests | ||
from django.core.cache import cache | ||
|
||
from apps.base.utils import live_settings | ||
from apps.phone_notifications.exceptions import FailedToMakeCall, FailedToStartVerification | ||
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/" | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class ZvonokPhoneProvider(PhoneProvider): | ||
""" | ||
ZvonokPhoneProvider is an implementation of phone provider which supports only voice calls (zvonok.com). | ||
API docs: https://api-docs.zvonok.com/ . Call status description: https://zvonok.com/ru-ru/guide/guide_statuses/ | ||
""" | ||
|
||
def make_notification_call(self, number: str, message: str) -> ZvonokPhoneCall: | ||
speaker = None | ||
body = None | ||
|
||
if live_settings.ZVONOK_AUDIO_ID: | ||
message = f'<audio id="{live_settings.ZVONOK_AUDIO_ID}"/>' | ||
else: | ||
speaker = live_settings.ZVONOK_SPEAKER_ID | ||
|
||
try: | ||
response = self._call_create(number, message, speaker) | ||
response.raise_for_status() | ||
body = response.json() | ||
if not body: | ||
logger.error(f"ZvonokPhoneProvider.make_notification_call: failed, empty body") | ||
raise FailedToMakeCall(graceful_msg=f"Failed make notification call to {number}, empty body") | ||
call_id = body.get("call_id") | ||
|
||
if not call_id: | ||
logger.error(f"ZvonokPhoneProvider.make_notification_call: failed, missing call id") | ||
raise FailedToMakeCall(graceful_msg=self._get_graceful_msg(body, number)) | ||
|
||
logger.info(f"ZvonokPhoneProvider.make_notification_call: success, call_id {call_id}") | ||
|
||
return ZvonokPhoneCall( | ||
status=ZvonokCallStatuses.IN_PROCESS, | ||
call_id=call_id, | ||
campaign_id=live_settings.ZVONOK_CAMPAIGN_ID, | ||
) | ||
|
||
except requests.exceptions.HTTPError as http_err: | ||
logger.error(f"ZvonokPhoneProvider.make_notification_call: failed {http_err}") | ||
raise FailedToMakeCall(graceful_msg=self._get_graceful_msg(body, number)) | ||
except (requests.exceptions.ConnectionError, requests.exceptions.JSONDecodeError, TypeError) as err: | ||
logger.error(f"ZvonokPhoneProvider.make_notification_call: failed {err}") | ||
raise FailedToMakeCall(graceful_msg=f"Failed make notification call to {number}") | ||
|
||
def make_call(self, number: str, message: str): | ||
body = None | ||
speaker = live_settings.ZVONOK_SPEAKER_ID | ||
|
||
try: | ||
response = self._call_create(number, message, speaker) | ||
response.raise_for_status() | ||
body = response.json() | ||
if not body: | ||
logger.error(f"ZvonokPhoneProvider.make_call: failed, empty body") | ||
raise FailedToMakeCall(graceful_msg=f"Failed make call to {number}, empty body") | ||
|
||
call_id = body.get("call_id") | ||
|
||
if not call_id: | ||
raise FailedToMakeCall(graceful_msg=self._get_graceful_msg(body, number)) | ||
logger.info(f"ZvonokPhoneProvider.make_call: success, call_id {call_id}") | ||
|
||
except requests.exceptions.HTTPError as http_err: | ||
logger.error(f"ZvonokPhoneProvider.make_call: failed {http_err}") | ||
raise FailedToMakeCall(graceful_msg=self._get_graceful_msg(body, number)) | ||
except (requests.exceptions.ConnectionError, requests.exceptions.JSONDecodeError, TypeError) as err: | ||
logger.error(f"ZvonokPhoneProvider.make_call: failed {err}") | ||
raise FailedToMakeCall(graceful_msg=f"Failed make call to {number}") | ||
|
||
def _call_create(self, number: str, text: str, speaker: Optional[str] = None): | ||
params = { | ||
"public_key": live_settings.ZVONOK_API_KEY, | ||
"campaign_id": live_settings.ZVONOK_CAMPAIGN_ID, | ||
"phone": number, | ||
"text": text, | ||
} | ||
|
||
if speaker: | ||
params["speaker"] = speaker | ||
|
||
return requests.post(ZVONOK_CALL_URL, params=params) | ||
|
||
def _get_graceful_msg(self, body, number): | ||
if body: | ||
status = body.get("status") | ||
data = body.get("data") | ||
if status == "error" and data: | ||
return f"Failed make call to {number} with error: {data}" | ||
return f"Failed make call to {number}" | ||
|
||
def make_verification_call(self, number: str): | ||
code = str(randint(100000, 999999)) | ||
cache.set(self._cache_key(number), code, timeout=10 * 60) | ||
codewspaces = " ".join(code) | ||
|
||
body = None | ||
speaker = live_settings.ZVONOK_SPEAKER_ID | ||
|
||
try: | ||
response = self._call_create(number, f"Your verification code is {codewspaces}", speaker) | ||
response.raise_for_status() | ||
body = response.json() | ||
if not body: | ||
logger.error(f"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)) | ||
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)) | ||
except (requests.exceptions.ConnectionError, requests.exceptions.JSONDecodeError, TypeError) as err: | ||
logger.error(f"ZvonokPhoneProvider.make_verification_call: failed {err}") | ||
raise FailedToStartVerification(graceful_msg=f"Failed make verification call to {number}") | ||
|
||
def finish_verification(self, number, code): | ||
has = cache.get(self._cache_key(number)) | ||
if has is not None and has == code: | ||
return number | ||
else: | ||
return None | ||
|
||
def _cache_key(self, number): | ||
return f"zvonok_provider_{number}" | ||
|
||
@property | ||
def flags(self) -> ProviderFlags: | ||
return ProviderFlags( | ||
configured=True, | ||
test_sms=False, | ||
test_call=True, | ||
verification_call=True, | ||
verification_sms=False, | ||
) |
Oops, something went wrong.