Skip to content

Commit

Permalink
Drop non-critical SMSes during DND time for Indian recipients (#1856)
Browse files Browse the repository at this point in the history
  • Loading branch information
jace authored Aug 21, 2023
1 parent 952e8b6 commit 9002e5f
Show file tree
Hide file tree
Showing 12 changed files with 94 additions and 24 deletions.
2 changes: 1 addition & 1 deletion funnel/devtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ def mock_sms(
# Patch email
install_mock(transports.email.send.send_email, mock_email)
# Patch SMS
install_mock(transports.sms.send, mock_sms)
install_mock(transports.sms.send.send_sms, mock_sms)

return worker(*args, **kwargs)

Expand Down
25 changes: 22 additions & 3 deletions funnel/transports/sms/send.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
import phonenumbers
import requests
from flask import url_for
from pytz import timezone
from twilio.base.exceptions import TwilioRestException
from twilio.rest import Client

from baseframe import _
from coaster.utils import utcnow

from ... import app
from ...models import PhoneNumber, PhoneNumberBlockedError, sa
Expand All @@ -22,17 +24,19 @@
TransportRecipientError,
TransportTransactionError,
)
from .template import SmsTemplate
from .template import SmsPriority, SmsTemplate

__all__ = [
'make_exotel_token',
'validate_exotel_token',
'send_via_exotel',
'send_via_twilio',
'send',
'send_sms',
'init',
]

indian_timezone = timezone('Asia/Kolkata')


@dataclass
class SmsSender:
Expand Down Expand Up @@ -97,6 +101,14 @@ def validate_exotel_token(token: str, to: str) -> bool:
return True


def okay_to_message_in_india_right_now() -> bool:
"""Report if it's currently within messaging hours in India (9 AM to 7PM IST)."""
now = utcnow().astimezone(indian_timezone)
if now.hour >= 9 and now.hour < 19:
return True
return False


def send_via_exotel(
phone: Union[str, phonenumbers.PhoneNumber, PhoneNumber],
message: SmsTemplate,
Expand Down Expand Up @@ -125,6 +137,13 @@ def send_via_exotel(
"Dropping SMS message with unknown template id: %s", str(message)
)
return ''
if (
message.message_priority in (SmsPriority.OPTIONAL, SmsPriority.NORMAL)
and not okay_to_message_in_india_right_now()
):
# TODO: Implement deferred sending for `NORMAL` priority
app.logger.warning("Dropping SMS message in DND time: %s", str(message))
return ''
payload['DltTemplateId'] = message.registered_templateid
if callback:
payload['StatusCallback'] = url_for(
Expand Down Expand Up @@ -270,7 +289,7 @@ def init() -> bool:
return bool(senders_by_prefix)


def send(
def send_sms(
phone: Union[str, phonenumbers.PhoneNumber, PhoneNumber],
message: SmsTemplate,
callback: bool = True,
Expand Down
12 changes: 12 additions & 0 deletions funnel/transports/sms/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
from __future__ import annotations

import re
from enum import Enum
from string import Formatter
from typing import Any, ClassVar, Dict, Optional, Pattern, cast

from flask import Flask

__all__ = [
'DLT_VAR_MAX_LENGTH',
'SmsPriority',
'SmsTemplate',
'WebOtpTemplate',
'OneLineTemplate',
Expand All @@ -31,6 +33,13 @@
DLT_VAR_MAX_LENGTH = 30


class SmsPriority(Enum):
URGENT = 1 # For OTPs and time-sensitive messages
IMPORTANT = 2 # For messaging any time of the day, including during DND hours
OPTIONAL = 3 # Okay to drop this message if not sent at a good time
NORMAL = 4 # Everything else, will not be sent during DND hours, TODO requeue


class SmsTemplate:
r"""
SMS template validator and formatter, for DLT registered SMS in India.
Expand Down Expand Up @@ -121,6 +130,8 @@ def truncate(self):
template: ClassVar[str] = ""
#: Optional plaintext Python template without validation against registered template
plaintext_template: ClassVar[str] = ""
#: Message delivery priority
message_priority: ClassVar[SmsPriority] = SmsPriority.NORMAL

#: Autogenerated regex version of registered template, will be updated in subclasses
registered_template_re: ClassVar[Pattern] = re.compile('')
Expand Down Expand Up @@ -400,6 +411,7 @@ class WebOtpTemplate(SmsTemplate):
"OTP is {otp} for Hasgeek. If you did not request this, report misuse at"
" https://has.gy/not-my-otp\n\n@hasgeek.com #{otp}"
)
message_priority = SmsPriority.URGENT


class OneLineTemplate(SmsTemplate):
Expand Down
2 changes: 1 addition & 1 deletion funnel/views/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ def dispatch_transport_sms(
# the worker may be delayed and the user may have changed their preference.
user_notification.messageid_sms = 'cancelled'
return
user_notification.messageid_sms = sms.send(
user_notification.messageid_sms = sms.send_sms(
str(view.transport_for('sms')), view.sms_with_unsubscribe()
)
statsd.incr(
Expand Down
11 changes: 7 additions & 4 deletions funnel/views/notifications/comment_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@
Proposal,
User,
)
from ...transports.sms import OneLineTemplate, SmsTemplate
from ...transports.sms import OneLineTemplate, SmsPriority, SmsTemplate
from ..helpers import shortlink
from ..notification import RenderNotification
from .mixins import TemplateVarMixin


class CommentReplyTemplate(TemplateVarMixin, SmsTemplate):
"""DLT registered template for RSVP without a next session."""
"""DLT registered template for a reply to a comment."""

registered_template = (
'{#var#} has replied to your comment: {#var#}'
Expand All @@ -40,12 +40,13 @@ class CommentReplyTemplate(TemplateVarMixin, SmsTemplate):
'\n\nhttps://bye.li to stop -Hasgeek'
)
plaintext_template = '{actor} has replied to your comment: {url}'
message_priority = SmsPriority.NORMAL

url: str


class CommentProposalTemplate(TemplateVarMixin, SmsTemplate):
"""DLT registered template for RSVP without a next session."""
"""DLT registered template for a comment on a proposal."""

registered_template = (
'{#var#} commented on your submission: {#var#}'
Expand All @@ -56,12 +57,13 @@ class CommentProposalTemplate(TemplateVarMixin, SmsTemplate):
'\n\nhttps://bye.li to stop -Hasgeek'
)
plaintext_template = '{actor} commented on your submission: {url}'
message_priority = SmsPriority.NORMAL

url: str


class CommentProjectTemplate(TemplateVarMixin, SmsTemplate):
"""DLT registered template for RSVP without a next session."""
"""DLT registered template for a comment on a project."""

registered_template = (
'{#var#} commented on a project you are in: {#var#}'
Expand All @@ -72,6 +74,7 @@ class CommentProjectTemplate(TemplateVarMixin, SmsTemplate):
'\n\nhttps://bye.li to stop -Hasgeek'
)
plaintext_template = '{actor} commented on a project you are in: {url}'
message_priority = SmsPriority.NORMAL

url: str

Expand Down
3 changes: 2 additions & 1 deletion funnel/views/notifications/project_starting_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from baseframe.filters import time_filter

from ...models import Project, ProjectStartingNotification, Session
from ...transports.sms import SmsTemplate
from ...transports.sms import SmsPriority, SmsTemplate
from ..helpers import shortlink
from ..notification import RenderNotification
from .mixins import TemplateVarMixin
Expand All @@ -28,6 +28,7 @@ class ProjectStartingTemplate(TemplateVarMixin, SmsTemplate):
"\n\nhttps://bye.li to stop - Hasgeek"
)
plaintext_template = "Reminder: {project} is starting soon. Join at {url}"
message_priority = SmsPriority.IMPORTANT

project_only: str
url: str
Expand Down
6 changes: 4 additions & 2 deletions funnel/views/notifications/proposal_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
ProposalSubmittedNotification,
sa,
)
from ...transports.sms import SmsTemplate
from ...transports.sms import SmsPriority, SmsTemplate
from ..helpers import shortlink
from ..notification import RenderNotification
from .mixins import TemplateVarMixin


class ProposalReceivedTemplate(TemplateVarMixin, SmsTemplate):
"""DLT registered template for Proposal received."""
"""DLT registered template for proposal received."""

registered_template = (
"There's a new submission from {#var#} in {#var#}."
Expand All @@ -33,6 +33,7 @@ class ProposalReceivedTemplate(TemplateVarMixin, SmsTemplate):
plaintext_template = (
"There's a new submission from {actor} in {project}. Read it here: {url}"
)
message_priority = SmsPriority.NORMAL

url: str

Expand All @@ -51,6 +52,7 @@ class ProposalSubmittedTemplate(TemplateVarMixin, SmsTemplate):
plaintext_template = (
"{project} has received your submission. Here's the link to share: {url}"
)
message_priority = SmsPriority.IMPORTANT

url: str

Expand Down
4 changes: 3 additions & 1 deletion funnel/views/notifications/rsvp_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
Rsvp,
)
from ...transports import email
from ...transports.sms import MessageTemplate, SmsTemplate
from ...transports.sms import MessageTemplate, SmsPriority, SmsTemplate
from ..helpers import shortlink
from ..notification import RenderNotification
from ..schedule import schedule_ical
Expand All @@ -35,6 +35,7 @@ class RegistrationConfirmationTemplate(TemplateVarMixin, SmsTemplate):
"\n\nhttps://bye.li to stop - Hasgeek"
)
plaintext_template = "You have registered for {project} {url}"
message_priority = SmsPriority.IMPORTANT

datetime: str
url: str
Expand All @@ -56,6 +57,7 @@ class RegistrationConfirmationWithNextTemplate(TemplateVarMixin, SmsTemplate):
plaintext_template = (
"You have registered for {project}, scheduled for {datetime}. {url}"
)
message_priority = SmsPriority.IMPORTANT

datetime: str
url: str
Expand Down
3 changes: 2 additions & 1 deletion funnel/views/notifications/update_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from baseframe import _, __

from ...models import NewUpdateNotification, Update, User
from ...transports.sms import SmsTemplate
from ...transports.sms import SmsPriority, SmsTemplate
from ..helpers import shortlink
from ..notification import RenderNotification
from .mixins import TemplateVarMixin
Expand All @@ -23,6 +23,7 @@ class UpdateTemplate(TemplateVarMixin, SmsTemplate):
"There is an update in {profile}: {url}\n\nhttps://bye.li to stop -Hasgeek"
)
plaintext_template = "There is an update in {profile}: {url}"
message_priority = SmsPriority.NORMAL

url: str

Expand Down
4 changes: 2 additions & 2 deletions funnel/views/otp.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ def send_sms(
msg = SmsMessage(phone=self.phone, message=str(template_message))
try:
# Now send this
msg.transactionid = sms.send(
msg.transactionid = sms.send_sms(
phone=msg.phone_number, message=template_message
)
except TransportRecipientError as exc:
Expand Down Expand Up @@ -339,7 +339,7 @@ def send_sms(
msg = SmsMessage(phone=self.phone, message=str(template_message))
try:
# Now send this
msg.transactionid = sms.send(
msg.transactionid = sms.send_sms(
phone=msg.phone_number, message=template_message
)
except TransportRecipientError as exc:
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/views/login_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
TEST_OAUTH_TOKEN = 'test_oauth_token' # nosec

# Functions to patch to capture OTPs
PATCH_SMS_SEND = 'funnel.transports.sms.send'
PATCH_SMS_SEND = 'funnel.transports.sms.send_sms'
PATCH_SMS_OTP = 'funnel.views.otp.OtpSession.send_sms'
PATCH_SMS_OTP_LOGIN = 'funnel.views.otp.OtpSessionForLogin.send_sms'
PATCH_EMAIL_OTP_LOGIN = 'funnel.views.otp.OtpSessionForLogin.send_email'
Expand Down
Loading

0 comments on commit 9002e5f

Please sign in to comment.