From aeb35009bebf59da220a4570b10540786d6a49f9 Mon Sep 17 00:00:00 2001 From: Andrey Oleynik Date: Wed, 5 Jul 2023 08:55:53 +0300 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + docs/sources/open-source/_index.md | 28 +++- engine/apps/base/models/live_setting.py | 19 +++ engine/apps/zvonok/__init__.py | 0 engine/apps/zvonok/migrations/0001_initial.py | 30 ++++ engine/apps/zvonok/migrations/__init__.py | 0 engine/apps/zvonok/models/__init__.py | 1 + engine/apps/zvonok/models/phone_call.py | 74 +++++++++ engine/apps/zvonok/phone_provider.py | 151 ++++++++++++++++++ engine/apps/zvonok/status_callback.py | 86 ++++++++++ engine/apps/zvonok/urls.py | 7 + engine/apps/zvonok/views.py | 52 ++++++ engine/engine/urls.py | 1 + engine/settings/base.py | 13 +- 14 files changed, 461 insertions(+), 2 deletions(-) create mode 100644 engine/apps/zvonok/__init__.py create mode 100644 engine/apps/zvonok/migrations/0001_initial.py create mode 100644 engine/apps/zvonok/migrations/__init__.py create mode 100644 engine/apps/zvonok/models/__init__.py create mode 100644 engine/apps/zvonok/models/phone_call.py create mode 100644 engine/apps/zvonok/phone_provider.py create mode 100644 engine/apps/zvonok/status_callback.py create mode 100644 engine/apps/zvonok/urls.py create mode 100644 engine/apps/zvonok/views.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 33d517c59f..1c186afd98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/docs/sources/open-source/_index.md b/docs/sources/open-source/_index.md index 2abce68358..10fcc2c315 100644 --- a/docs/sources/open-source/_index.md +++ b/docs/sources/open-source/_index.md @@ -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 diff --git a/engine/apps/base/models/live_setting.py b/engine/apps/base/models/live_setting.py index 34600ddbd2..f5a6f8e79f 100644 --- a/engine/apps/base/models/live_setting.py +++ b/engine/apps/base/models/live_setting.py @@ -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): diff --git a/engine/apps/zvonok/__init__.py b/engine/apps/zvonok/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/engine/apps/zvonok/migrations/0001_initial.py b/engine/apps/zvonok/migrations/0001_initial.py new file mode 100644 index 0000000000..b6a0091922 --- /dev/null +++ b/engine/apps/zvonok/migrations/0001_initial.py @@ -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, + }, + ), + ] diff --git a/engine/apps/zvonok/migrations/__init__.py b/engine/apps/zvonok/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/engine/apps/zvonok/models/__init__.py b/engine/apps/zvonok/models/__init__.py new file mode 100644 index 0000000000..11f80b0789 --- /dev/null +++ b/engine/apps/zvonok/models/__init__.py @@ -0,0 +1 @@ +from .phone_call import ZvonokCallStatuses, ZvonokPhoneCall # noqa: F401 diff --git a/engine/apps/zvonok/models/phone_call.py b/engine/apps/zvonok/models/phone_call.py new file mode 100644 index 0000000000..20e581f32d --- /dev/null +++ b/engine/apps/zvonok/models/phone_call.py @@ -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, + ) diff --git a/engine/apps/zvonok/phone_provider.py b/engine/apps/zvonok/phone_provider.py new file mode 100644 index 0000000000..e49d8525b1 --- /dev/null +++ b/engine/apps/zvonok/phone_provider.py @@ -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'