Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Phone provider refactoring #1713

Merged
merged 55 commits into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
f1e9b30
Remove unused "created_for_slack" method
Konstantinov-Innokentii Mar 25, 2023
2c8f83b
Initial commit for PhoneProvider refactoring
Konstantinov-Innokentii Apr 9, 2023
bbd1430
Delete a.py
Konstantinov-Innokentii Apr 9, 2023
ab928fa
Fixed receiving phone_call and sms notifications
Konstantinov-Innokentii Apr 25, 2023
e0cc0b1
Cleam twilioapp utils
Konstantinov-Innokentii Apr 25, 2023
81178a6
Clean some comments
Konstantinov-Innokentii Apr 25, 2023
0ccd284
Comments and logging
Konstantinov-Innokentii Apr 25, 2023
678a23d
Polishing
Konstantinov-Innokentii Apr 26, 2023
f66242f
Remove non-related changed
Konstantinov-Innokentii Apr 26, 2023
b53558e
Return AllowOnlyTwilio permission
Konstantinov-Innokentii Apr 26, 2023
3b99554
Reduce twilio status callbacks
Konstantinov-Innokentii Apr 26, 2023
21d589e
Keep status and sid
Konstantinov-Innokentii Apr 26, 2023
56d6ccd
Remove unised SMSMessage model
Konstantinov-Innokentii Apr 26, 2023
9ffaa24
Fix migration dependencies
Konstantinov-Innokentii Apr 26, 2023
7fd7600
Merge dev
Konstantinov-Innokentii Apr 26, 2023
8dd39f9
Keep TwilioLogRecord for backward compalibility
Konstantinov-Innokentii Apr 26, 2023
26ebc43
Count oss call in limits
Konstantinov-Innokentii Apr 26, 2023
3b8479e
Count oss call in limits
Konstantinov-Innokentii Apr 26, 2023
381aa54
Rename OnCallPhoneCall/SMS to PhoneCall/SMSRecord
Konstantinov-Innokentii May 5, 2023
4d79515
Rewrite twilio phone call tests
Konstantinov-Innokentii May 5, 2023
bb5b505
Fix existing tests
Konstantinov-Innokentii May 5, 2023
d3ad2ea
Tests for PhoneBackend
Konstantinov-Innokentii May 9, 2023
b6cbd7d
Inherit TwilioSMS from ProviderSMS
Konstantinov-Innokentii May 9, 2023
ae09bbe
Test PhoneBackend.relay_oss_sms/call
Konstantinov-Innokentii May 9, 2023
bfa71ce
Replace doube quotes in PhoneCallTemplates
Konstantinov-Innokentii May 9, 2023
7c8df3d
Tests for phone verification
Konstantinov-Innokentii May 9, 2023
bf3ec04
Tests for TwilioPhoneProvider
Konstantinov-Innokentii May 10, 2023
db33b14
automock apply_async for all tests
Konstantinov-Innokentii May 10, 2023
59ec9a9
Merge branch 'dev' into phone_notificator
Konstantinov-Innokentii May 10, 2023
1be1305
Add PhoneProvider.Config
Konstantinov-Innokentii May 17, 2023
6f017da
Merge branch 'dev' into phone_notificator
Konstantinov-Innokentii May 17, 2023
67929a3
Fix TwilioProvider config
Konstantinov-Innokentii May 17, 2023
128c549
Rename provider config to flags
Konstantinov-Innokentii May 18, 2023
ea0a414
Fixes
Konstantinov-Innokentii May 22, 2023
f589b53
Comment iteration
Konstantinov-Innokentii May 22, 2023
6147dfd
Enable cloud calls only in OSS
Konstantinov-Innokentii May 22, 2023
81c2490
Comments polishing
Konstantinov-Innokentii May 22, 2023
2eca39e
Merge branch 'dev' into phone_notificator
Konstantinov-Innokentii May 22, 2023
a513a0e
verification with call and send test sms button have been added to Us…
Ukochka May 22, 2023
163aeb3
Handle ProviderNotSupports in verification API
Konstantinov-Innokentii May 23, 2023
a38f4a8
Fixes
Konstantinov-Innokentii May 23, 2023
4f16a9b
Merge branch 'dev' into phone_notificator
Konstantinov-Innokentii May 23, 2023
7f0e6d4
Comment out simple phone provider
Konstantinov-Innokentii May 23, 2023
b9a6402
Merge remote-tracking branch 'origin/phone_notificator' into phone_no…
Konstantinov-Innokentii May 23, 2023
2ea310b
Ignore migration linter
Konstantinov-Innokentii May 23, 2023
24d9eb0
Fix tests
Konstantinov-Innokentii May 23, 2023
3298392
Update CHANGELOG.md
Konstantinov-Innokentii May 23, 2023
7acb39c
Fix tests
Konstantinov-Innokentii May 23, 2023
3987336
Fix tests
Konstantinov-Innokentii May 23, 2023
9b71018
Fix tests
Konstantinov-Innokentii May 23, 2023
e65a3c3
Fix MockPhoneProvider
Konstantinov-Innokentii May 23, 2023
0585099
Make migrations backward compatible
Konstantinov-Innokentii May 24, 2023
7bebc85
Merge branch 'dev' into phone_notificator
Konstantinov-Innokentii May 24, 2023
2e336f9
Fix tests
Konstantinov-Innokentii May 24, 2023
0a227d4
Merge remote-tracking branch 'origin/phone_notificator' into phone_no…
Konstantinov-Innokentii May 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion engine/apps/alerts/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ class ActionSource:
(
SLACK,
WEB,
TWILIO,
PHONE,
TELEGRAM,
) = range(4)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from apps.alerts.incident_appearance.templaters.alert_templater import AlertTemplater
from common.utils import clean_markup, escape_for_twilio_phone_call
from common.utils import clean_markup


class AlertPhoneCallTemplater(AlertTemplater):
Expand All @@ -14,14 +14,11 @@ def _postformat(self, templated_alert):
return templated_alert

def _postformat_pipeline(self, text):
return self._escape(clean_markup(self._slack_format_for_phone_call(text))) if text is not None else text
return clean_markup(self._slack_format_for_phone_call(text)) if text is not None else text

def _slack_format_for_phone_call(self, data):
sf = self.slack_formatter
sf.user_mention_format = "{}"
sf.channel_mention_format = "#{}"
sf.hyperlink_mention_format = "{title}"
return sf.format(data)

def _escape(self, data):
return escape_for_twilio_phone_call(data)
Copy link
Contributor

@iskhakov iskhakov Apr 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

20 changes: 5 additions & 15 deletions engine/apps/alerts/tasks/notify_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from apps.alerts.constants import NEXT_ESCALATION_DELAY
from apps.alerts.signals import user_notification_action_triggered_signal
from apps.base.messaging import get_messaging_backend_from_id
from apps.base.utils import live_settings
from apps.phone_notifications.phone_backend import PhoneBackend
from common.custom_celery_tasks import shared_dedicated_queue_retry_task

from .task_logger import task_logger
Expand Down Expand Up @@ -224,8 +224,6 @@ def notify_user_task(
autoretry_for=(Exception,), retry_backoff=True, max_retries=1 if settings.DEBUG else None
)
def perform_notification(log_record_pk):
SMSMessage = apps.get_model("twilioapp", "SMSMessage")
PhoneCall = apps.get_model("twilioapp", "PhoneCall")
UserNotificationPolicy = apps.get_model("base", "UserNotificationPolicy")
TelegramToUserConnector = apps.get_model("telegram", "TelegramToUserConnector")
UserNotificationPolicyLogRecord = apps.get_model("base", "UserNotificationPolicyLogRecord")
Expand Down Expand Up @@ -259,20 +257,12 @@ def perform_notification(log_record_pk):
return

if notification_channel == UserNotificationPolicy.NotificationChannel.SMS:
SMSMessage.send_sms(
user,
alert_group,
notification_policy,
is_cloud_notification=live_settings.GRAFANA_CLOUD_NOTIFICATIONS_ENABLED,
)
phone_backend = PhoneBackend()
phone_backend.notify_by_sms(user, alert_group, notification_policy)

elif notification_channel == UserNotificationPolicy.NotificationChannel.PHONE_CALL:
PhoneCall.make_call(
user,
alert_group,
notification_policy,
is_cloud_notification=live_settings.GRAFANA_CLOUD_NOTIFICATIONS_ENABLED,
)
phone_backend = PhoneBackend()
phone_backend.notify_by_call(user, alert_group, notification_policy)

elif notification_channel == UserNotificationPolicy.NotificationChannel.TELEGRAM:
TelegramToUserConnector.notify_user(user, alert_group, notification_policy)
Expand Down
2 changes: 1 addition & 1 deletion engine/apps/api/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
from apps.base.models import UserNotificationPolicy
from apps.base.utils import live_settings
from apps.oss_installation.utils import cloud_user_identity_status
from apps.twilioapp.utils import check_phone_number_is_valid
from apps.user_management.models import User
from apps.user_management.models.user import default_working_hours
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
from common.api_helpers.mixins import EagerLoadingMixin
from common.api_helpers.utils import check_phone_number_is_valid
from common.timezones import TimeZoneField

from .custom_serializers import DynamicFieldsModelSerializer
Expand Down
99 changes: 64 additions & 35 deletions engine/apps/api/views/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,16 @@
from apps.base.messaging import get_messaging_backend_from_id
from apps.base.utils import live_settings
from apps.mobile_app.auth import MobileAppAuthTokenAuthentication
from apps.phone_notifications.exceptions import (
FailedToMakeCall,
FailedToStartVerification,
NumberAlreadyVerified,
NumberNotVerified,
ProviderNotSupports,
)
from apps.phone_notifications.phone_backend import PhoneBackend
from apps.telegram.client import TelegramClient
from apps.telegram.models import TelegramVerificationCode
from apps.twilioapp.phone_manager import PhoneManager
from apps.twilioapp.twilio_client import twilio_client
from apps.user_management.models import Team, User
from common.api_helpers.exceptions import Conflict
from common.api_helpers.mixins import FilterSerializerMixin, PublicPrimaryKeyMixin
Expand Down Expand Up @@ -291,22 +297,44 @@ def timezone_options(self, request):
throttle_classes=[GetPhoneVerificationCodeThrottlerPerUser, GetPhoneVerificationCodeThrottlerPerOrg],
)
def get_verification_code(self, request, pk):

logger.info("get_verification_code: validating reCAPTCHA code")
# valid = recaptcha.check_recaptcha_internal_api(request, "mobile_verification_code")
valid = check_recaptcha_internal_api(request, "mobile_verification_code")
if not valid:
logger.warning(f"get_verification_code: invalid reCAPTCHA validation")
return Response("failed reCAPTCHA check", status=status.HTTP_400_BAD_REQUEST)
logger.info('get_verification_code: pass reCAPTCHA validation"')

user = self.get_object()
phone_manager = PhoneManager(user)
code_sent = phone_manager.send_verification_code()
phone_backend = PhoneBackend()
try:
phone_backend.send_verification_sms(user)
except NumberAlreadyVerified:
return Response("Phone number already verified", status=status.HTTP_400_BAD_REQUEST)
except FailedToStartVerification:
return Response("Something went wrong while sending code", status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response(status=status.HTTP_200_OK)

if not code_sent:
logger.warning(f"Mobile app verification code was not successfully sent")
return Response(status=status.HTTP_400_BAD_REQUEST)
@action(
detail=True,
methods=["get"],
throttle_classes=[GetPhoneVerificationCodeThrottlerPerUser, GetPhoneVerificationCodeThrottlerPerOrg],
)
def get_verification_call(self, request, pk):
logger.info("get_verification_code_via_call: validating reCAPTCHA code")
valid = check_recaptcha_internal_api(request, "mobile_verification_code")
if not valid:
logger.warning(f"get_verification_code_via_call: invalid reCAPTCHA validation")
return Response("failed reCAPTCHA check", status=status.HTTP_400_BAD_REQUEST)
logger.info('get_verification_code_via_call: pass reCAPTCHA validation"')

user = self.get_object()
phone_backend = PhoneBackend()
try:
phone_backend.make_verification_call(user)
except NumberAlreadyVerified:
return Response("Phone number already verified", status=status.HTTP_400_BAD_REQUEST)
except FailedToStartVerification:
return Response("Something went wrong while calling", status=status.HTTP_500_INTERNAL_SERVER_ERROR)
Comment on lines +351 to +371
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new endpoint?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, it is introduced keeping in mind that some providers can't sent sms, like asterisk. Twilio is also capable of verifying via call, but I didn't supported it to not overcomplicate refactoring.

return Response(status=status.HTTP_200_OK)

@action(
Expand All @@ -320,29 +348,31 @@ def verify_number(self, request, pk):
if not code:
return Response("Invalid verification code", status=status.HTTP_400_BAD_REQUEST)
prev_state = target_user.insight_logs_serialized
phone_manager = PhoneManager(target_user)
verified, error = phone_manager.verify_phone_number(code)

if not verified:
return Response(error, status=status.HTTP_400_BAD_REQUEST)
new_state = target_user.insight_logs_serialized
write_resource_insight_log(
instance=target_user,
author=self.request.user,
event=EntityEvent.UPDATED,
prev_state=prev_state,
new_state=new_state,
)
return Response(status=status.HTTP_200_OK)

phone_backend = PhoneBackend()
verified = phone_backend.verify_phone_number(target_user, code)
if verified:
new_state = target_user.insight_logs_serialized
write_resource_insight_log(
instance=target_user,
author=self.request.user,
event=EntityEvent.UPDATED,
prev_state=prev_state,
new_state=new_state,
)
return Response(status=status.HTTP_200_OK)
else:
return Response("Verification code is not correct", status=status.HTTP_400_BAD_REQUEST)

@action(detail=True, methods=["put"])
def forget_number(self, request, pk):
target_user = self.get_object()
prev_state = target_user.insight_logs_serialized
phone_manager = PhoneManager(target_user)
forget = phone_manager.forget_phone_number()

if forget:
phone_backend = PhoneBackend()
removed = phone_backend.forget_number(target_user)

if removed:
new_state = target_user.insight_logs_serialized
write_resource_insight_log(
instance=target_user,
Expand All @@ -356,18 +386,17 @@ def forget_number(self, request, pk):
@action(detail=True, methods=["post"], throttle_classes=[TestCallThrottler])
def make_test_call(self, request, pk):
user = self.get_object()
phone_number = user.verified_phone_number

if phone_number is None:
return Response(status=status.HTTP_400_BAD_REQUEST)

try:
twilio_client.make_test_call(to=phone_number)
except Exception as e:
logger.error(f"Unable to make a test call due to {e}")
phone_backend = PhoneBackend()
phone_backend.make_test_call(user)
except NumberNotVerified:
return Response("Phone number is not verified", status=status.HTTP_400_BAD_REQUEST)
except FailedToMakeCall:
return Response(
data="Something went wrong while making a test call", status=status.HTTP_500_INTERNAL_SERVER_ERROR
"Something went wrong while making a test call", status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
except ProviderNotSupports:
return Response("Phone provider not supports phone calls", status=status.HTTP_500_INTERNAL_SERVER_ERROR)

return Response(status=status.HTTP_200_OK)

Expand Down
2 changes: 2 additions & 0 deletions engine/apps/base/models/live_setting.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class LiveSetting(models.Model):
"GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED",
"GRAFANA_CLOUD_NOTIFICATIONS_ENABLED",
"DANGEROUS_WEBHOOKS_ENABLED",
"PHONE_PROVIDER",
)

DESCRIPTIONS = {
Expand Down Expand Up @@ -146,6 +147,7 @@ class LiveSetting(models.Model):
"GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED": "Enable heartbeat integration with Grafana Cloud OnCall.",
"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()))}",
}

SECRET_SETTING_NAMES = (
Expand Down
Empty file.
34 changes: 34 additions & 0 deletions engine/apps/phone_notifications/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
class FailedToMakeCall(Exception):
pass


class FailedToSendSMS(Exception):
pass


class NumberNotVerified(Exception):
pass


class NumberAlreadyVerified(Exception):
pass


class FailedToStartVerification(Exception):
pass


class FailedToFinishVerification(Exception):
pass


class ProviderNotSupports(Exception):
pass


class CallsLimitExceeded(Exception):
pass


class SMSLimitExceeded(Exception):
pass
64 changes: 64 additions & 0 deletions engine/apps/phone_notifications/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Generated by Django 3.2.18 on 2023-04-08 07:40

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
('base', '0003_delete_organizationlogrecord'),
('alerts', '0010_channelfilter_filtering_term_type'),
('user_management', '0010_team_is_sharing_resources_to_all'),
('twilioapp', '0003_auto_20230408_0711')
]

state_operations = [
migrations.CreateModel(
name='OnCallPhoneCall',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('exceeded_limit', models.BooleanField(default=None, null=True)),
('status', models.PositiveSmallIntegerField(blank=True, choices=[(10, 'queued'), (20, 'ringing'), (30, 'in-progress'), (40, 'completed'), (50, 'busy'), (60, 'failed'), (70, 'no-answer'), (80, 'canceled')], null=True)),
('sid', models.CharField(blank=True, max_length=50)),
('created_at', models.DateTimeField(auto_now_add=True)),
('grafana_cloud_notification', models.BooleanField(default=False)),
('notification_policy', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='base.usernotificationpolicy')),
('receiver', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='user_management.user')),
('represents_alert', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='alerts.alert')),
('represents_alert_group', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='alerts.alertgroup')),
],
),
migrations.CreateModel(
name='OnCallSMS',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('exceeded_limit', models.BooleanField(default=None, null=True)),
('status', models.PositiveSmallIntegerField(blank=True,
choices=[(10, 'accepted'), (20, 'queued'), (30, 'sending'),
(40, 'sent'), (50, 'failed'), (60, 'delivered'),
(70, 'undelivered'), (80, 'receiving'),
(90, 'received'), (100, 'read')], null=True)),
('grafana_cloud_notification', models.BooleanField(default=False)),
('sid', models.CharField(blank=True, max_length=50)),
('created_at', models.DateTimeField(auto_now_add=True)),
('notification_policy',
models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL,
to='base.usernotificationpolicy')),
('receiver', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE,
to='user_management.user')),
('represents_alert',
models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL,
to='alerts.alert')),
('represents_alert_group',
models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL,
to='alerts.alertgroup')),
],
),
]

operations = [
migrations.SeparateDatabaseAndState(state_operations=state_operations)
]
Empty file.
2 changes: 2 additions & 0 deletions engine/apps/phone_notifications/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .phone_call import OnCallPhoneCall
from .sms import OnCallSMS
Loading