add zvonok integration (#2339)
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>
This commit is contained in:
parent
5cc9d5441f
commit
aeb35009be
14 changed files with 461 additions and 2 deletions
|
|
@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Added
|
||||
|
||||
- Add full avatar URL for on-call users in schedule internal API by @vadimkerr ([#2414](https://github.com/grafana/oncall/pull/2414))
|
||||
- Add phone call using the zvonok.com service by @sreway ([#2339](https://github.com/grafana/oncall/pull/2339))
|
||||
|
||||
## v1.3.3 (2023-06-29)
|
||||
|
||||
|
|
|
|||
|
|
@ -207,7 +207,9 @@ The benefits of connecting to Grafana Cloud include:
|
|||
|
||||
To connect to Grafana Cloud, refer to the **Cloud** page in your OSS Grafana OnCall instance.
|
||||
|
||||
## Twilio Setup
|
||||
## Supported Phone Providers
|
||||
|
||||
### Twilio
|
||||
|
||||
Grafana OnCall supports Twilio SMS and phone call notifications delivery. If you prefer to configure SMS and phone call
|
||||
notifications using Twilio, complete the following steps:
|
||||
|
|
@ -215,6 +217,30 @@ notifications using Twilio, complete the following steps:
|
|||
1. Set `GRAFANA_CLOUD_NOTIFICATIONS_ENABLED` as **False** to ensure the Grafana OSS <-> Cloud connector is disabled.
|
||||
1. From your **OnCall** environment, select **Env Variables** and configure all variables starting with `TWILIO_`.
|
||||
|
||||
### Zvonok.com
|
||||
|
||||
Grafana OnCall supports Zvonok.com phone call notifications delivery. To configure phone call notifications using
|
||||
Zvonok.com, complete the following steps:
|
||||
|
||||
1. Change `PHONE_PROVIDER` value to `zvonok`.
|
||||
2. Create a public API key on the Profile->Settings page, and assign its value to `ZVONOK_API_KEY`.
|
||||
3. Create campaign and assign its ID value to `ZVONOK_CAMPAIGN_ID`.
|
||||
4. If you are planning to use pre-recorded audio instead of a speech synthesizer, you can copy the ID of the audio clip
|
||||
to the variable `ZVONOK_AUDIO_ID` (optional step).
|
||||
5. To make a call with a specific voice, you can set the `ZVONOK_SPEAKER_ID`.
|
||||
By default, the ID used is `Salli` (optional step).
|
||||
6. 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}`
|
||||
|
||||
The names of the transmitted parameters can be redefined through environment variables:
|
||||
|
||||
- `ZVONOK_POSTBACK_CALL_ID` - call id (ct_call_id) query parameter name
|
||||
- `ZVONOK_POSTBACK_CAMPAIGN_ID` - company id (ct_campaign_id) query parameter name
|
||||
- `ZVONOK_POSTBACK_STATUS` - status (ct_status) query parameter name
|
||||
- `ZVONOK_POSTBACK_USER_CHOICE` - user choice (ct_user_choice) query parameter name
|
||||
- `ZVONOK_POSTBACK_USER_CHOICE_ACK` - user choice (ct_user_choice) query parameter value for acknowledge alert group
|
||||
|
||||
## Email Setup
|
||||
|
||||
Grafana OnCall is capable of sending emails using SMTP as a user notification step. To setup email notifications, populate
|
||||
|
|
|
|||
|
|
@ -60,6 +60,15 @@ class LiveSetting(models.Model):
|
|||
"GRAFANA_CLOUD_NOTIFICATIONS_ENABLED",
|
||||
"DANGEROUS_WEBHOOKS_ENABLED",
|
||||
"PHONE_PROVIDER",
|
||||
"ZVONOK_API_KEY",
|
||||
"ZVONOK_CAMPAIGN_ID",
|
||||
"ZVONOK_AUDIO_ID",
|
||||
"ZVONOK_SPEAKER_ID",
|
||||
"ZVONOK_POSTBACK_CALL_ID",
|
||||
"ZVONOK_POSTBACK_CAMPAIGN_ID",
|
||||
"ZVONOK_POSTBACK_STATUS",
|
||||
"ZVONOK_POSTBACK_USER_CHOICE",
|
||||
"ZVONOK_POSTBACK_USER_CHOICE_ACK",
|
||||
)
|
||||
|
||||
DESCRIPTIONS = {
|
||||
|
|
@ -148,6 +157,15 @@ class LiveSetting(models.Model):
|
|||
"GRAFANA_CLOUD_NOTIFICATIONS_ENABLED": "Enable SMS/call notifications via Grafana Cloud OnCall",
|
||||
"DANGEROUS_WEBHOOKS_ENABLED": "Enable outgoing webhooks to private networks",
|
||||
"PHONE_PROVIDER": f"Phone provider name. Available options: {','.join(list(settings.PHONE_PROVIDERS.keys()))}",
|
||||
"ZVONOK_API_KEY": "API public key. You can get it in Profile->Settings section.",
|
||||
"ZVONOK_CAMPAIGN_ID": "Calls by API campaign ID. You can get it after campaign creation.",
|
||||
"ZVONOK_AUDIO_ID": "Calls with specific audio. You can get it in Audioclips section.",
|
||||
"ZVONOK_SPEAKER_ID": "Calls with speaker.",
|
||||
"ZVONOK_POSTBACK_CALL_ID": "'Postback' call id (ct_call_id) query parameter name to validate a postback request.",
|
||||
"ZVONOK_POSTBACK_CAMPAIGN_ID": "'Postback' company id (ct_campaign_id) query parameter name to validate a postback request.",
|
||||
"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).",
|
||||
}
|
||||
|
||||
SECRET_SETTING_NAMES = (
|
||||
|
|
@ -163,6 +181,7 @@ class LiveSetting(models.Model):
|
|||
"SLACK_SIGNING_SECRET",
|
||||
"TELEGRAM_TOKEN",
|
||||
"GRAFANA_CLOUD_ONCALL_TOKEN",
|
||||
"ZVONOK_API_KEY",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
|
|
|
|||
0
engine/apps/zvonok/__init__.py
Normal file
0
engine/apps/zvonok/__init__.py
Normal file
30
engine/apps/zvonok/migrations/0001_initial.py
Normal file
30
engine/apps/zvonok/migrations/0001_initial.py
Normal file
|
|
@ -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,
|
||||
},
|
||||
),
|
||||
]
|
||||
0
engine/apps/zvonok/migrations/__init__.py
Normal file
0
engine/apps/zvonok/migrations/__init__.py
Normal file
1
engine/apps/zvonok/models/__init__.py
Normal file
1
engine/apps/zvonok/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .phone_call import ZvonokCallStatuses, ZvonokPhoneCall # noqa: F401
|
||||
74
engine/apps/zvonok/models/phone_call.py
Normal file
74
engine/apps/zvonok/models/phone_call.py
Normal file
|
|
@ -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,
|
||||
)
|
||||
151
engine/apps/zvonok/phone_provider.py
Normal file
151
engine/apps/zvonok/phone_provider.py
Normal file
|
|
@ -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,
|
||||
)
|
||||
86
engine/apps/zvonok/status_callback.py
Normal file
86
engine/apps/zvonok/status_callback.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from django.apps import apps
|
||||
|
||||
from apps.alerts.constants import ActionSource
|
||||
from apps.alerts.signals import user_notification_action_triggered_signal
|
||||
from apps.base.utils import live_settings
|
||||
from apps.zvonok.models.phone_call import ZvonokCallStatuses, ZvonokPhoneCall
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def update_zvonok_call_status(call_id: str, call_status: str, user_choice: Optional[str] = None):
|
||||
UserNotificationPolicyLogRecord = apps.get_model("base", "UserNotificationPolicyLogRecord")
|
||||
|
||||
status_code = ZvonokCallStatuses.DETERMINANT.get(call_status)
|
||||
if status_code is None:
|
||||
logger.warning(f"zvonok.update_zvonok_call_status: unexpected status call_id={call_id} status={call_status}")
|
||||
return
|
||||
|
||||
zvonok_phone_call = ZvonokPhoneCall.objects.filter(call_id=call_id).first()
|
||||
if zvonok_phone_call is None:
|
||||
logger.warning(f"zvonok.update_zvonok_call_status: zvonok_phone_call not found call_id={call_id}")
|
||||
return
|
||||
|
||||
logger.info(f"zvonok.update_zvonok_call_status: found zvonok_phone_call call_id={call_id}")
|
||||
|
||||
zvonok_phone_call.status = status_code
|
||||
zvonok_phone_call.save(update_fields=["status"])
|
||||
phone_call_record = zvonok_phone_call.phone_call_record
|
||||
|
||||
if phone_call_record is None:
|
||||
logger.warning(
|
||||
f"zvonok.update_zvonok_call_status: zvonok_phone_call has no phone_call record call_id={call_id} "
|
||||
f"status={call_status}"
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"zvonok.update_zvonok_call_status: found phone_call_record id={phone_call_record.id} "
|
||||
f"call_id={call_id} status={call_status}"
|
||||
)
|
||||
log_record_type = None
|
||||
log_record_error_code = None
|
||||
|
||||
success_statuses = [ZvonokCallStatuses.USER, ZvonokCallStatuses.COMPL_FINISHED]
|
||||
|
||||
if status_code in success_statuses:
|
||||
log_record_type = UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_SUCCESS
|
||||
else:
|
||||
log_record_type = UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED
|
||||
log_record_error_code = UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_PHONE_CALL_FAILED
|
||||
|
||||
if log_record_type is not None:
|
||||
log_record = UserNotificationPolicyLogRecord(
|
||||
type=log_record_type,
|
||||
notification_error_code=log_record_error_code,
|
||||
author=phone_call_record.receiver,
|
||||
notification_policy=phone_call_record.notification_policy,
|
||||
alert_group=phone_call_record.represents_alert_group,
|
||||
notification_step=phone_call_record.notification_policy.step
|
||||
if phone_call_record.notification_policy
|
||||
else None,
|
||||
notification_channel=phone_call_record.notification_policy.notify_by
|
||||
if phone_call_record.notification_policy
|
||||
else None,
|
||||
)
|
||||
log_record.save()
|
||||
logger.info(
|
||||
f"zvonok.update_zvonok_call_status: created log_record log_record_id={log_record.id} "
|
||||
f"type={log_record_type}"
|
||||
)
|
||||
|
||||
user_notification_action_triggered_signal.send(sender=update_zvonok_call_status, log_record=log_record)
|
||||
|
||||
if user_choice and user_choice == live_settings.ZVONOK_POSTBACK_USER_CHOICE_ACK:
|
||||
alert_group = phone_call_record.represents_alert_group
|
||||
user = phone_call_record.receiver
|
||||
logger.info(
|
||||
f"zvonok.update_zvonok_call_status: processing user choice"
|
||||
f" phone_call_record id={phone_call_record.id} zvonok_phone_call_id={call_id} "
|
||||
f"alert_group_id={alert_group.id} user_id={user.id}"
|
||||
)
|
||||
|
||||
alert_group.acknowledge_by_user(user, action_source=ActionSource.PHONE)
|
||||
7
engine/apps/zvonok/urls.py
Normal file
7
engine/apps/zvonok/urls.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from django.urls import path
|
||||
|
||||
from .views import CallStatusCallback
|
||||
|
||||
urlpatterns = [
|
||||
path("call_status_events", CallStatusCallback.as_view(), name="call_status_events"),
|
||||
]
|
||||
52
engine/apps/zvonok/views.py
Normal file
52
engine/apps/zvonok/views.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
from django.apps import apps
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import BasePermission
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from apps.base.utils import live_settings
|
||||
|
||||
from .status_callback import update_zvonok_call_status
|
||||
|
||||
|
||||
class AllowOnlyZvonok(BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
call_id = request.GET.get(live_settings.ZVONOK_POSTBACK_CALL_ID)
|
||||
if not call_id:
|
||||
return False
|
||||
|
||||
campaign_id = request.GET.get(live_settings.ZVONOK_POSTBACK_CAMPAIGN_ID)
|
||||
if not campaign_id:
|
||||
return False
|
||||
|
||||
if campaign_id != live_settings.ZVONOK_CAMPAIGN_ID:
|
||||
return False
|
||||
ZvonokCall = apps.get_model("zvonok", "ZvonokPhoneCall")
|
||||
call = ZvonokCall.objects.filter(call_id=call_id, campaign_id=campaign_id).first()
|
||||
if call:
|
||||
return self.validate_request(request)
|
||||
return False
|
||||
|
||||
def validate_request(self, request):
|
||||
if request.GET.get(live_settings.ZVONOK_POSTBACK_STATUS):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# Receive Call Status from Zvonok
|
||||
class CallStatusCallback(APIView):
|
||||
permission_classes = [AllowOnlyZvonok]
|
||||
|
||||
def get(self, request):
|
||||
self._handle_call_status(request)
|
||||
return Response(data="", status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def post(self, request):
|
||||
self._handle_call_status(request)
|
||||
return Response(data="", status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def _handle_call_status(self, request):
|
||||
call_id = request.GET.get(live_settings.ZVONOK_POSTBACK_CALL_ID)
|
||||
call_status = request.GET.get(live_settings.ZVONOK_POSTBACK_STATUS)
|
||||
user_choice = request.GET.get(live_settings.ZVONOK_POSTBACK_USER_CHOICE)
|
||||
update_zvonok_call_status(call_id=call_id, call_status=call_status, user_choice=user_choice)
|
||||
|
|
@ -65,6 +65,7 @@ if settings.FEATURE_SLACK_INTEGRATION_ENABLED:
|
|||
if settings.IS_OPEN_SOURCE:
|
||||
urlpatterns += [
|
||||
path("api/internal/v1/", include("apps.oss_installation.urls", namespace="oss_installation")),
|
||||
path("zvonok/", include("apps.zvonok.urls")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
|
|
|
|||
|
|
@ -682,7 +682,7 @@ INSTALLED_ONCALL_INTEGRATIONS = [
|
|||
]
|
||||
|
||||
if IS_OPEN_SOURCE:
|
||||
INSTALLED_APPS += ["apps.oss_installation"] # noqa
|
||||
INSTALLED_APPS += ["apps.oss_installation", "apps.zvonok"] # noqa
|
||||
|
||||
CELERY_BEAT_SCHEDULE["send_usage_stats"] = { # noqa
|
||||
"task": "apps.oss_installation.tasks.send_usage_stats_report",
|
||||
|
|
@ -724,5 +724,16 @@ PYROSCOPE_AUTH_TOKEN = os.getenv("PYROSCOPE_AUTH_TOKEN", "")
|
|||
PHONE_PROVIDERS = {
|
||||
"twilio": "apps.twilioapp.phone_provider.TwilioPhoneProvider",
|
||||
# "simple": "apps.phone_notifications.simple_phone_provider.SimplePhoneProvider",
|
||||
"zvonok": "apps.zvonok.phone_provider.ZvonokPhoneProvider",
|
||||
}
|
||||
PHONE_PROVIDER = os.environ.get("PHONE_PROVIDER", default="twilio")
|
||||
|
||||
ZVONOK_API_KEY = os.getenv("ZVONOK_API_KEY", None)
|
||||
ZVONOK_CAMPAIGN_ID = os.getenv("ZVONOK_CAMPAIGN_ID", None)
|
||||
ZVONOK_AUDIO_ID = os.getenv("ZVONOK_AUDIO_ID", None)
|
||||
ZVONOK_SPEAKER_ID = os.getenv("ZVONOK_SPEAKER_ID", "Salli")
|
||||
ZVONOK_POSTBACK_CALL_ID = os.getenv("ZVONOK_POSTBACK_CALL_ID", "call_id")
|
||||
ZVONOK_POSTBACK_CAMPAIGN_ID = os.getenv("ZVONOK_POSTBACK_CAMPAIGN_ID", "campaign_id")
|
||||
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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue