From 97096b46634c11685ebd56c48824d4b9caccab62 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Fri, 30 Jun 2023 11:20:16 +0200 Subject: [PATCH 01/10] update oss image build drone step --- .drone.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.drone.yml b/.drone.yml index 2fd504cb..5b076a34 100644 --- a/.drone.yml +++ b/.drone.yml @@ -236,10 +236,15 @@ steps: - name: build and push docker image image: thegeeklab/drone-docker-buildx:24.1.0 + # From the docs (https://drone-plugin-index.geekdocs.de/plugins/drone-docker-buildx/) regarding privileged=true: + # + # Be aware that the this plugin requires privileged capabilities, otherwise the integrated + # Docker daemon is not able to start. + privileged: true settings: repo: grafana/oncall - tags: ${DRONE_TAG} - platform: linux/arm64/v8,linux/amd64 + tags: latest,${DRONE_TAG} + platforms: linux/arm64/v8,linux/amd64 dockerfile: engine/Dockerfile target: prod context: engine/ @@ -330,5 +335,3 @@ name: drone_token --- kind: signature hmac: 08184a4c1313a5e6cbca2370d20a6a8c5d71726b4148b5c3ed1396a21bebaa37 - -... From 44e1bef2502b7802719948743ef3835162f66677 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Fri, 30 Jun 2023 14:45:40 +0100 Subject: [PATCH 02/10] Add full avatar URL for on-call users in schedule internal API (#2414) # What this PR does Adds full avatar URL for on-call users in schedule internal API (`avatar_full`). ## 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] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required) --- CHANGELOG.md | 6 ++++ engine/apps/api/tests/test_schedules.py | 33 ++++++++++++++++++++++ engine/apps/user_management/models/user.py | 7 ++++- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9951042e..41e27a46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Add full avatar URL for on-call users in schedule internal API by @vadimkerr ([#2414](https://github.com/grafana/oncall/pull/2414)) + ## v1.3.3 (2023-06-29) ### Added diff --git a/engine/apps/api/tests/test_schedules.py b/engine/apps/api/tests/test_schedules.py index cf08592f..69ae02dc 100644 --- a/engine/apps/api/tests/test_schedules.py +++ b/engine/apps/api/tests/test_schedules.py @@ -1858,3 +1858,36 @@ def test_get_schedule_from_other_team_with_flag( response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_get_schedule_on_call_now( + make_organization, make_user_for_organization, make_token_for_organization, make_schedule, make_user_auth_headers +): + organization = make_organization(grafana_url="https://example.com") + user = make_user_for_organization(organization, username="test", avatar_url="/avatar/test123") + _, token = make_token_for_organization(organization) + + schedule = make_schedule( + organization, + schedule_class=OnCallScheduleWeb, + name="test_web_schedule", + ) + + client = APIClient() + url = reverse("api-internal:schedule-list") + with patch( + "apps.schedules.models.on_call_schedule.OnCallScheduleQuerySet.get_oncall_users", + return_value={schedule.pk: [user]}, + ): + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + + assert response.status_code == status.HTTP_200_OK + assert response.json()["results"][0]["on_call_now"] == [ + { + "pk": user.public_primary_key, + "username": "test", + "avatar": "/avatar/test123", + "avatar_full": "https://example.com/avatar/test123", + } + ] diff --git a/engine/apps/user_management/models/user.py b/engine/apps/user_management/models/user.py index 931f1bcb..5ca8c27f 100644 --- a/engine/apps/user_management/models/user.py +++ b/engine/apps/user_management/models/user.py @@ -270,7 +270,12 @@ class User(models.Model): self._timezone = value def short(self): - return {"username": self.username, "pk": self.public_primary_key, "avatar": self.avatar_url} + return { + "username": self.username, + "pk": self.public_primary_key, + "avatar": self.avatar_url, + "avatar_full": self.avatar_full_url, + } # Insight logs @property From 44d0252ef11c613077b6b40a6636376c9f87973d Mon Sep 17 00:00:00 2001 From: Innokentii Konstantinov Date: Tue, 4 Jul 2023 15:01:30 +0800 Subject: [PATCH 03/10] Remove test push dynamic setting (#2416) --- engine/apps/api/views/features.py | 9 --------- engine/apps/mobile_app/demo_push.py | 10 ++++++---- engine/apps/mobile_app/tests/test_demo_push.py | 14 +++++++------- .../MobileAppConnection/MobileAppConnection.tsx | 2 +- grafana-plugin/src/state/features.ts | 1 - 5 files changed, 14 insertions(+), 22 deletions(-) diff --git a/engine/apps/api/views/features.py b/engine/apps/api/views/features.py index 1c619d25..b0e41897 100644 --- a/engine/apps/api/views/features.py +++ b/engine/apps/api/views/features.py @@ -14,7 +14,6 @@ FEATURE_GRAFANA_CLOUD_NOTIFICATIONS = "grafana_cloud_notifications" FEATURE_GRAFANA_CLOUD_CONNECTION = "grafana_cloud_connection" FEATURE_WEB_SCHEDULES = "web_schedules" FEATURE_WEBHOOKS2 = "webhooks2" -FEATURE_MOBILE_TEST_PUSH = "mobile_test_push" class FeaturesAPIView(APIView): @@ -64,12 +63,4 @@ class FeaturesAPIView(APIView): if is_webhooks_enabled_for_organization(request.auth.organization.pk): enabled_features.append(FEATURE_WEBHOOKS2) - enabled_mobile_test_push = DynamicSetting.objects.get_or_create( - name="enabled_mobile_test_push", - defaults={"boolean_value": False}, - )[0] - - if enabled_mobile_test_push.boolean_value: - enabled_features.append(FEATURE_MOBILE_TEST_PUSH) - return enabled_features diff --git a/engine/apps/mobile_app/demo_push.py b/engine/apps/mobile_app/demo_push.py index 96ace3f9..1b6bcce2 100644 --- a/engine/apps/mobile_app/demo_push.py +++ b/engine/apps/mobile_app/demo_push.py @@ -9,8 +9,6 @@ from apps.mobile_app.exceptions import DeviceNotSet from apps.mobile_app.tasks import FCMMessageData, MessageType, _construct_fcm_message, _send_push_notification, logger from apps.user_management.models import User -TEST_PUSH_TITLE = "Hi, this is a test notification from Grafana OnCall" - def send_test_push(user, critical=False): device_to_notify = FCMDevice.objects.filter(user=user).first() @@ -42,7 +40,7 @@ def _get_test_escalation_fcm_message(user: User, device_to_notify: FCMDevice, cr ) + MobileAppUserSettings.IOS_SOUND_NAME_EXTENSION # iOS app expects the filename to have an extension fcm_message_data: FCMMessageData = { - "title": TEST_PUSH_TITLE, + "title": get_test_push_title(critical), # Pass user settings, so the Android app can use them to play the correct sound and volume "default_notification_sound_name": ( mobile_app_user_settings.default_notification_sound_name @@ -68,7 +66,7 @@ def _get_test_escalation_fcm_message(user: User, device_to_notify: FCMDevice, cr apns_payload = APNSPayload( aps=Aps( thread_id=thread_id, - alert=ApsAlert(title=TEST_PUSH_TITLE), + alert=ApsAlert(title=get_test_push_title(critical)), sound=CriticalSound( # The notification shouldn't be critical if the user has disabled "override DND" setting critical=overrideDND, @@ -84,3 +82,7 @@ def _get_test_escalation_fcm_message(user: User, device_to_notify: FCMDevice, cr message_type = MessageType.CRITICAL if critical else MessageType.NORMAL return _construct_fcm_message(message_type, device_to_notify, thread_id, fcm_message_data, apns_payload) + + +def get_test_push_title(critical: bool) -> str: + return f"Hi, this is a {'critical ' if critical else ''}test notification from Grafana OnCall" diff --git a/engine/apps/mobile_app/tests/test_demo_push.py b/engine/apps/mobile_app/tests/test_demo_push.py index 35843529..cb6ae51a 100644 --- a/engine/apps/mobile_app/tests/test_demo_push.py +++ b/engine/apps/mobile_app/tests/test_demo_push.py @@ -1,7 +1,7 @@ import pytest from fcm_django.models import FCMDevice -from apps.mobile_app.demo_push import TEST_PUSH_TITLE, _get_test_escalation_fcm_message +from apps.mobile_app.demo_push import _get_test_escalation_fcm_message, get_test_push_title from apps.mobile_app.models import MobileAppUserSettings @@ -33,8 +33,8 @@ def test_test_escalation_fcm_message_user_settings( # Check expected test push content assert message.apns.payload.aps.badge is None - assert message.apns.payload.aps.alert.title == TEST_PUSH_TITLE - assert message.data["title"] == TEST_PUSH_TITLE + assert message.apns.payload.aps.alert.title == get_test_push_title(critical=False) + assert message.data["title"] == get_test_push_title(critical=False) @pytest.mark.django_db @@ -66,8 +66,8 @@ def test_escalation_fcm_message_user_settings_critical( # Check expected test push content assert message.apns.payload.aps.badge is None - assert message.apns.payload.aps.alert.title == TEST_PUSH_TITLE - assert message.data["title"] == TEST_PUSH_TITLE + assert message.apns.payload.aps.alert.title == get_test_push_title(critical=True) + assert message.data["title"] == get_test_push_title(critical=True) @pytest.mark.django_db @@ -91,5 +91,5 @@ def test_escalation_fcm_message_user_settings_critical_override_dnd_disabled( # Check expected test push content assert message.apns.payload.aps.badge is None - assert message.apns.payload.aps.alert.title == TEST_PUSH_TITLE - assert message.data["title"] == TEST_PUSH_TITLE + assert message.apns.payload.aps.alert.title == get_test_push_title(critical=True) + assert message.data["title"] == get_test_push_title(critical=True) diff --git a/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx b/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx index 26ed1353..d0d8f004 100644 --- a/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx +++ b/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx @@ -196,7 +196,7 @@ const MobileAppConnection = observer(({ userPk }: Props) => { {content} - {store.hasFeature(AppFeature.MobileTestPush) && mobileAppIsCurrentlyConnected && isCurrentUser && ( + {mobileAppIsCurrentlyConnected && isCurrentUser && (
{ profile } + data-testid="view-users-missing-permission-message" severity="info" /> )} From 5cc9d5441fcbd02c5f088e954399eb30cff47c15 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Tue, 4 Jul 2023 15:49:30 -0400 Subject: [PATCH 05/10] include note about docker-compose rabbitmq version bump --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41e27a46..33d517c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Change permissions used during setup to better represent actions being taken by @mderynck ([#2242](https://github.com/grafana/oncall/pull/2242)) - Display 100000+ in stats when there are more than 100000 alert groups in the result ([#1901](https://github.com/grafana/oncall/pull/1901)) - Change OnCall plugin to use service accounts and api tokens for communicating with backend, by @mderynck ([#2385](https://github.com/grafana/oncall/pull/2385)) +- RabbitMQ Docker image upgraded from 3.7.19 to 3.12.0 in `docker-compose-developer.yml` and + `docker-compose-mysql-rabbitmq.yml`. **Note**: if you use one of these config files for your deployment + you _may_ need to follow the RabbitMQ "upgrade steps" listed [here](https://rabbitmq.com/upgrade.html#rabbitmq-version-upgradability) + by @joeyorlando ([#2359](https://github.com/grafana/oncall/pull/2359)) ### Fixed From aeb35009bebf59da220a4570b10540786d6a49f9 Mon Sep 17 00:00:00 2001 From: Andrey Oleynik Date: Wed, 5 Jul 2023 08:55:53 +0300 Subject: [PATCH 06/10] 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 33d517c5..1c186afd 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 2abce683..10fcc2c3 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 34600ddb..f5a6f8e7 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 00000000..e69de29b diff --git a/engine/apps/zvonok/migrations/0001_initial.py b/engine/apps/zvonok/migrations/0001_initial.py new file mode 100644 index 00000000..b6a00919 --- /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 00000000..e69de29b diff --git a/engine/apps/zvonok/models/__init__.py b/engine/apps/zvonok/models/__init__.py new file mode 100644 index 00000000..11f80b07 --- /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 00000000..20e581f3 --- /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 00000000..e49d8525 --- /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'