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

Phone provider refactoring #1713

merged 55 commits into from
May 24, 2023

Conversation

Konstantinov-Innokentii
Copy link
Member

@Konstantinov-Innokentii Konstantinov-Innokentii commented Apr 9, 2023

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 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.

@Konstantinov-Innokentii Konstantinov-Innokentii requested a review from a team April 9, 2023 16:18
@Konstantinov-Innokentii Konstantinov-Innokentii changed the title Phone notificator Phone provider refactoring Apr 9, 2023
Remove a.py
@Konstantinov-Innokentii Konstantinov-Innokentii marked this pull request as draft April 9, 2023 16:21

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.

Copy link
Contributor

@iskhakov iskhakov left a comment

Choose a reason for hiding this comment

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

looks good



# Duplicate to avoid circular import
class TwilioCallStatuses:
Copy link
Contributor

Choose a reason for hiding this comment

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

This will be removed after migration, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I'm keeping it to be able to receive Gather and call statuses(digits from phone call) for calls which were made shortly before migration.



def get_gather_url():
return create_engine_url(reverse("twilioapp:gather"), "https://pretty-mosh-97.loca.lt")
Copy link
Contributor

Choose a reason for hiding this comment

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

hardcoded url

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks

database_operations=database_operations,
state_operations=state_operations
)
]
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider using https://docs.djangoproject.com/en/3.2/ref/models/options/#db-table instead of renaming tables to avoid data migrations

Copy link
Member

@vadimkerr vadimkerr left a comment

Choose a reason for hiding this comment

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

Great refactoring, looks like it will make adding new providers so much easier 👍

)


class OnCallPhoneCall(models.Model):
Copy link
Member

Choose a reason for hiding this comment

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

nit: maybe remove "OnCall" from the class name so it's just PhoneCall?

Copy link
Member Author

Choose a reason for hiding this comment

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

I tried just PhoneCall name, but it doesn't works
For example we have this method in ProveProvider interface. If I rename OnCallPhoneCall to PhoneCall, then I should rename oncall_phone_call parameter name to phone_call which makes no sense in function singnature. On the other side I don't like PhoneCall model name with oncall_phone_call parameter, since it is confusing.

def make_notification_call(self, number, message, oncall_phone_call):
        ....

Same issue we have with TwilioPhoneCall model. Is has field oncall_phone_call. Renaming it to just phone call leads to lines like twilio_phone_call.phone_call.
I'm not 100% happy with OnCallPhoneCall name, but having just PhoneCall introduces more confusion.

Copy link
Member Author

Choose a reason for hiding this comment

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

@vadimkerr what do you think about PhoneCallRecord instead of OnCallPhoneCall. Also it sounds well: twilio_phone_call.phone_call_record ( at least better that twilio_phone_call.oncall_phone_call)

)


class OnCallSMS(models.Model):
Copy link
Member

Choose a reason for hiding this comment

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

nit: Maybe just SMS?

# Used in get_phone_provider function to dynamically load current provider.
PHONE_PROVIDERS = {
"twilio": "apps.twilioapp.phone_provider.TwilioPhoneProvider",
"simple": "apps.phone_notifications.simple_phone_provider.SimplePhoneProvider",
Copy link
Member

Choose a reason for hiding this comment

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

This should probably be removed so it doesn't show up as an available option for PHONE_PROVIDER

Copy link
Contributor

Choose a reason for hiding this comment

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

could this still be useful?

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually it is useful only for slack now. We process events triggered from slack synchronously, instead of running celery tasks. It is needed to make re-rendering of slack message faster, when button is pressed. We don't use this system for other chatops integrations, but ideally we should. Just keeping it consistent, waiting when we will refactor messaging backends.

Copy link
Contributor

@joeyorlando joeyorlando left a comment

Choose a reason for hiding this comment

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

this is great! LGTM.

few minor questions/comments:

  • do we need the new get_verification_call endpoint?
  • I see you have "refactor tests" listed as a todo, +1 on this



def get_gather_message():
return "Press 1 to acknowledge, 2 to resolve, 3 to silence to 30 minutes"
Copy link
Contributor

Choose a reason for hiding this comment

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

nit move "1", "2", "3" into variables and reference those in the 3 places in this file. ex:

ACK_DIGIT = "1"
RESOLVE_DIGIT = "2"
SILENCE_DIGIT = "3"

def get_gather_message():
    return f"Press {ACK_DIGIT} to acknowledge, {RESOLVE_DIGIT} to resolve, {SILENCE_DIGIT} to silence to 30 minutes"

same thing for the process_digit and process_gather_data functions

Copy link
Contributor

Choose a reason for hiding this comment

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

🎉🎉🎉 this is really clean, nicely done

Copy link
Contributor

Choose a reason for hiding this comment

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

I assume the majority of this logic was just relocated from Twilio specific code?

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



def get_call_status_callback_url():
return create_engine_url(reverse("twilioapp:call_status_events"), "https://pretty-mosh-97.loca.lt")
Copy link
Contributor

Choose a reason for hiding this comment

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

hardcoded URL



def get_sms_status_callback_url():
return create_engine_url(reverse("twilioapp:sms_status_events"), "https://pretty-mosh-97.loca.lt")
Copy link
Contributor

Choose a reason for hiding this comment

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

hardcoded URL

Copy link
Contributor

Choose a reason for hiding this comment

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

should this file be deleted?

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, I'm still using it as a reference while working on PR, will delete it before merge, thanks!

Comment on lines +320 to +340
@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)
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.

@Ukochka Ukochka requested a review from a team May 22, 2023 14:11
@Konstantinov-Innokentii Konstantinov-Innokentii added this pull request to the merge queue May 24, 2023
Merged via the queue into dev with commit 1f786e8 May 24, 2023
@Konstantinov-Innokentii Konstantinov-Innokentii deleted the phone_notificator branch May 24, 2023 06:32
mderynck added a commit that referenced this pull request May 24, 2023
mderynck added a commit that referenced this pull request May 25, 2023
Many countries are introducing different requirements for SMS senders to
register and/or use alpha numeric ids, short codes or regional numbers
or face being blocked. The changes in this PR will give us more
flexibility by allowing us to map to different resources in Twilio based
on the phone number we are trying to reach. For this first
implementation the selection is made based on country code of the
recipient. Verification and phone calls were given the same treatment
although the immediate need is for SMS. Senders with no country code set
can be used as catch-all defaults. This also falls back to the
configured live settings/environment variables if not configured.

Possible future additions:
- Move through list of trying multiple senders before failing
notification
- Easily expanded to allow per-organization or per-user resources to let
users and tenants configure their own Twilio
- Add UI + replace live settings so users can configure their own
settings
- More selection criteria if needed

TODO:

- [x] Add+Fix Tests
- [x] Verify changes are compatible with #1713
brojd pushed a commit that referenced this pull request Sep 18, 2024
# 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](#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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pr:no public docs Added to a PR that does not require public documentation updates
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants