# What this PR does This PR moves phone notification logic into separate object PhoneBackend and introduces PhoneProvider interface to hide actual implementation of external phone services provider. It should allow add new phone providers just by implementing one class (See SimplePhoneProvider for example). # Why [Asterisk PR](https://github.com/grafana/oncall/pull/1282) showed that our phone notification system is not flexible. However this is one of the most frequent community questions - how to add "X" phone provider. Also, this refactoring move us one step closer to unifying all notification backends, since with PhoneBackend all phone notification logic is collected in one place and independent from concrete realisation. # Highligts 1. PhoneBackend object - contains all phone notifications business logic. 2. PhoneProvider - interface to external phone services provider. 3. TwilioPhoneProvider and SimplePhoneProvider - two examples of PhoneProvider implementation. 4. PhoneCallRecord and SMSRecord models. I introduced these models to keep phone notification limits logic decoupled from external providers. Existing TwilioPhoneCall and TwilioSMS objects will be migrated to the new table to not to reset limits counter. To be able to receive status callbacks and gather from Twilio TwilioPhoneCall and TwilioSMS still exists, but they are linked to PhoneCallRecord and SMSRecord via fk, to not to leat twilio logic into core code. --------- Co-authored-by: Yulia Shanyrova <yulia.shanyrova@grafana.com>
142 lines
6.5 KiB
Python
142 lines
6.5 KiB
Python
from django.apps import apps
|
|
from django.urls import reverse
|
|
|
|
from apps.alerts.signals import user_notification_action_triggered_signal
|
|
from apps.twilioapp.models import TwilioCallStatuses, TwilioPhoneCall, TwilioSMS, TwilioSMSstatuses
|
|
from common.api_helpers.utils import create_engine_url
|
|
|
|
|
|
def update_twilio_call_status(call_sid, call_status):
|
|
"""The function checks existence of TwilioPhoneCall instance
|
|
according to call_sid and updates status on message_status
|
|
|
|
Args:
|
|
call_sid (str): sid of Twilio call
|
|
call_status (str): new status
|
|
|
|
Returns:
|
|
|
|
"""
|
|
UserNotificationPolicyLogRecord = apps.get_model("base", "UserNotificationPolicyLogRecord")
|
|
|
|
if call_sid and call_status:
|
|
status = TwilioCallStatuses.DETERMINANT.get(call_status)
|
|
|
|
twilio_phone_call = TwilioPhoneCall.objects.filter(sid=call_sid).first()
|
|
|
|
# Check twilio phone call and then oncall phone call for backward compatibility after PhoneCall migration.
|
|
# Will be removed soon.
|
|
if twilio_phone_call:
|
|
status = TwilioCallStatuses.DETERMINANT.get(call_status)
|
|
twilio_phone_call.status = status
|
|
twilio_phone_call.save(update_fields=["status"])
|
|
phone_call_record = twilio_phone_call.phone_call_record
|
|
else:
|
|
PhoneCallRecord = apps.get_model("phone_notifications", "PhoneCallRecord")
|
|
phone_call_record = PhoneCallRecord.objects.filter(sid=call_sid).first()
|
|
|
|
if phone_call_record and status:
|
|
log_record_type = None
|
|
log_record_error_code = None
|
|
|
|
if status == TwilioCallStatuses.COMPLETED:
|
|
log_record_type = UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_SUCCESS
|
|
elif status in [TwilioCallStatuses.FAILED, TwilioCallStatuses.BUSY, TwilioCallStatuses.NO_ANSWER]:
|
|
log_record_type = UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED
|
|
log_record_error_code = get_error_code_by_twilio_status(status)
|
|
|
|
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,
|
|
)
|
|
user_notification_action_triggered_signal.send(sender=update_twilio_call_status, log_record=log_record)
|
|
|
|
|
|
def get_error_code_by_twilio_status(status):
|
|
UserNotificationPolicyLogRecord = apps.get_model("base", "UserNotificationPolicyLogRecord")
|
|
TWILIO_ERRORS_TO_ERROR_CODES_MAP = {
|
|
TwilioCallStatuses.BUSY: UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_PHONE_CALL_LINE_BUSY,
|
|
TwilioCallStatuses.FAILED: UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_PHONE_CALL_FAILED,
|
|
TwilioCallStatuses.NO_ANSWER: UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_PHONE_CALL_NO_ANSWER,
|
|
}
|
|
return TWILIO_ERRORS_TO_ERROR_CODES_MAP.get(status, None)
|
|
|
|
|
|
def update_twilio_sms_status(message_sid, message_status):
|
|
"""The function checks existence of SMSMessage
|
|
instance according to message_sid and updates status on
|
|
message_status
|
|
|
|
Args:
|
|
message_sid (str): sid of Twilio message
|
|
message_status (str): new status
|
|
|
|
Returns:
|
|
|
|
"""
|
|
UserNotificationPolicyLogRecord = apps.get_model("base", "UserNotificationPolicyLogRecord")
|
|
|
|
if message_sid and message_status:
|
|
status = TwilioSMSstatuses.DETERMINANT.get(message_status)
|
|
|
|
twilio_sms = TwilioSMS.objects.filter(sid=message_sid).first()
|
|
|
|
# Check twilio phone call and then oncall phone call for backward compatibility after PhoneCall migration.
|
|
# Will be removed soon.
|
|
if twilio_sms:
|
|
twilio_sms.status = status
|
|
twilio_sms.save(update_fields=["status"])
|
|
sms_record = twilio_sms.sms_record
|
|
else:
|
|
PhoneCallRecord = apps.get_model("phone_notifications", "PhoneCallRecord")
|
|
sms_record = PhoneCallRecord.objects.filter(sid=message_sid).first()
|
|
|
|
if sms_record and status:
|
|
log_record_type = None
|
|
log_record_error_code = None
|
|
if status == TwilioSMSstatuses.DELIVERED:
|
|
log_record_type = UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_SUCCESS
|
|
elif status in [TwilioSMSstatuses.UNDELIVERED, TwilioSMSstatuses.FAILED]:
|
|
log_record_type = UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED
|
|
log_record_error_code = get_sms_error_code_by_twilio_status(status)
|
|
|
|
if log_record_type is not None:
|
|
log_record = UserNotificationPolicyLogRecord(
|
|
type=log_record_type,
|
|
notification_error_code=log_record_error_code,
|
|
author=sms_record.receiver,
|
|
notification_policy=sms_record.notification_policy,
|
|
alert_group=sms_record.represents_alert_group,
|
|
notification_step=sms_record.notification_policy.step if sms_record.notification_policy else None,
|
|
notification_channel=sms_record.notification_policy.notify_by
|
|
if sms_record.notification_policy
|
|
else None,
|
|
)
|
|
user_notification_action_triggered_signal.send(sender=update_twilio_sms_status, log_record=log_record)
|
|
|
|
|
|
def get_sms_error_code_by_twilio_status(status):
|
|
UserNotificationPolicyLogRecord = apps.get_model("base", "UserNotificationPolicyLogRecord")
|
|
TWILIO_ERRORS_TO_ERROR_CODES_MAP = {
|
|
TwilioSMSstatuses.UNDELIVERED: UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_SMS_DELIVERY_FAILED,
|
|
TwilioSMSstatuses.FAILED: UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_SMS_DELIVERY_FAILED,
|
|
}
|
|
return TWILIO_ERRORS_TO_ERROR_CODES_MAP.get(status, None)
|
|
|
|
|
|
def get_call_status_callback_url():
|
|
return create_engine_url(reverse("twilioapp:call_status_events"))
|
|
|
|
|
|
def get_sms_status_callback_url():
|
|
return create_engine_url(reverse("twilioapp:sms_status_events"))
|