2020-05-13 10:37:41 +01:00
from datetime import datetime
2017-05-22 11:26:47 +01:00
from urllib . parse import unquote
2017-03-16 18:15:49 +00:00
2017-06-02 10:14:01 +01:00
import iso8601
2022-10-03 09:05:34 -07:00
from flask import Blueprint , abort , current_app , jsonify , request , json
2020-06-29 20:31:17 +01:00
from gds_metrics . metrics import Counter
2017-11-23 15:22:18 +00:00
from notifications_utils . recipients import try_validate_and_format_phone_number
2017-05-22 11:26:47 +01:00
2017-06-20 17:13:40 +01:00
from app . celery import tasks
2022-10-03 09:05:34 -07:00
from app . celery . validate_sns_message import sns_notification_handler
2017-07-19 13:50:29 +01:00
from app . config import QueueNames
2017-05-22 11:26:47 +01:00
from app . dao . inbound_sms_dao import dao_create_inbound_sms
2021-03-10 13:55:06 +00:00
from app . dao . services_dao import dao_fetch_service_by_inbound_number
2022-10-03 09:05:34 -07:00
from app . errors import register_errors , InvalidRequest
2021-03-10 13:55:06 +00:00
from app . models import INBOUND_SMS_TYPE , SMS_TYPE , InboundSms
2017-03-16 18:15:49 +00:00
receive_notifications_blueprint = Blueprint ( ' receive_notifications ' , __name__ )
register_errors ( receive_notifications_blueprint )
2020-06-29 20:31:17 +01:00
INBOUND_SMS_COUNTER = Counter (
' inbound_sms ' ,
' Total number of inbound SMS received ' ,
[ ' provider ' ]
)
2022-10-03 09:05:34 -07:00
@receive_notifications_blueprint.route ( ' /notifications/sms/receive/sns ' , methods = [ ' POST ' ] )
def receive_sns_sms ( ) :
"""
{
" originationNumber " : " +14255550182 " ,
" destinationNumber " : " +12125550101 " ,
" messageKeyword " : " JOIN " , # unique to our sending number
" messageBody " : " EXAMPLE " ,
" inboundMessageId " : " cae173d2-66b9-564c-8309-21f858e9fb84 " ,
" previousPublishedMessageId " : " wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY "
}
"""
try :
post_data = sns_notification_handler ( request . data , request . headers )
except Exception as e :
raise InvalidRequest ( f " SMS-SNS callback failed with error: { e } " , 400 )
message = json . loads ( post_data . get ( " Message " ) )
# TODO wrap this up
if " inboundMessageId " in message :
# TODO use standard formatting we use for all US numbers
inbound_number = message [ ' destinationNumber ' ] . replace ( ' + ' , ' ' )
service = fetch_potential_service ( inbound_number , ' sns ' )
if not service :
# since this is an issue with our service <-> number mapping, or no inbound_sms service permission
# we should still tell SNS that we received it successfully
current_app . logger . warning ( f " Mapping between service and inbound number: { inbound_number } is broken, or service does not have permission to receive inbound sms " )
return jsonify (
result = " success " , message = " SMS-SNS callback succeeded "
) , 200
INBOUND_SMS_COUNTER . labels ( " sns " ) . inc ( )
content = message . get ( " messageBody " )
from_number = message . get ( ' originationNumber ' )
provider_ref = message . get ( ' inboundMessageId ' )
date_received = post_data . get ( ' Timestamp ' )
provider_name = " sns "
inbound = create_inbound_sms_object ( service ,
content = content ,
from_number = from_number ,
provider_ref = provider_ref ,
date_received = date_received ,
provider_name = provider_name )
# TODO ensure inbound sms callback endpoints are accessible and functioning for notify api users, then uncomment the task below
tasks . send_inbound_sms_to_service . apply_async ( [ str ( inbound . id ) , str ( service . id ) ] , queue = QueueNames . NOTIFY )
current_app . logger . debug (
' {} received inbound SMS with reference {} from SNS ' . format ( service . id , inbound . provider_reference ) )
return jsonify (
result = " success " , message = " SMS-SNS callback succeeded "
) , 200
2020-06-29 20:31:17 +01:00
2017-03-16 18:15:49 +00:00
@receive_notifications_blueprint.route ( ' /notifications/sms/receive/mmg ' , methods = [ ' POST ' ] )
def receive_mmg_sms ( ) :
2017-05-22 11:26:47 +01:00
"""
{
' MSISDN ' : ' 447123456789 '
' Number ' : ' 40604 ' ,
' Message ' : ' some+uri+encoded+message % 3A ' ,
' ID ' : ' SOME-MMG-SPECIFIC-ID ' ,
' DateRecieved ' : ' 2017-05-21+11 % 3A56 % 3A11 '
}
"""
2017-03-16 18:15:49 +00:00
post_data = request . get_json ( )
2017-06-06 11:50:30 +01:00
2017-12-14 13:35:13 +00:00
auth = request . authorization
if not auth :
current_app . logger . warning ( " Inbound sms (MMG) no auth header " )
2017-12-19 15:00:51 +00:00
abort ( 401 )
2017-12-15 12:19:58 +00:00
elif auth . username not in current_app . config [ ' MMG_INBOUND_SMS_USERNAME ' ] \
or auth . password not in current_app . config [ ' MMG_INBOUND_SMS_AUTH ' ] :
2017-12-14 13:35:13 +00:00
current_app . logger . warning ( " Inbound sms (MMG) incorrect username ( {} ) or password " . format ( auth . username ) )
2017-12-19 15:00:51 +00:00
abort ( 403 )
2017-12-14 13:35:13 +00:00
2017-06-06 11:50:30 +01:00
inbound_number = strip_leading_forty_four ( post_data [ ' Number ' ] )
2017-08-23 13:03:52 +01:00
service = fetch_potential_service ( inbound_number , ' mmg ' )
if not service :
2017-06-29 15:33:44 +01:00
# since this is an issue with our service <-> number mapping, or no inbound_sms service permission
# we should still tell MMG that we received it successfully
2017-06-01 13:13:51 +01:00
return ' RECEIVED ' , 200
2020-06-29 20:31:17 +01:00
INBOUND_SMS_COUNTER . labels ( " mmg " ) . inc ( )
2017-05-22 11:26:47 +01:00
2017-06-20 17:13:40 +01:00
inbound = create_inbound_sms_object ( service ,
content = format_mmg_message ( post_data [ " Message " ] ) ,
from_number = post_data [ ' MSISDN ' ] ,
provider_ref = post_data [ " ID " ] ,
date_received = post_data . get ( ' DateRecieved ' ) ,
provider_name = " mmg " )
2017-05-22 11:26:47 +01:00
2017-06-20 17:13:40 +01:00
tasks . send_inbound_sms_to_service . apply_async ( [ str ( inbound . id ) , str ( service . id ) ] , queue = QueueNames . NOTIFY )
2017-05-22 11:26:47 +01:00
As Notify matures we probably need less logging, especially to report happy path events.
This PR is a proposal to reduce the average messages we see for a single notification from about 7 messages to 2.
Messaging would change to something like this:
February 2nd 2018, 15:39:05.885 Full delivery response from Firetext for notification: 8eda51d5-cd82-4569-bfc9-d5570cdf2126
{'status': ['0'], 'reference': ['8eda51d5-cd82-4569-bfc9-d5570cdf2126'], 'time': ['2018-02-02 15:39:01'], 'code': ['000']}
February 2nd 2018, 15:39:05.885 Firetext callback return status of 0 for reference: 8eda51d5-cd82-4569-bfc9-d5570cdf2126
February 2nd 2018, 15:38:57.727 SMS 8eda51d5-cd82-4569-bfc9-d5570cdf2126 sent to provider firetext at 2018-02-02 15:38:56.716814
February 2nd 2018, 15:38:56.727 Starting sending SMS 8eda51d5-cd82-4569-bfc9-d5570cdf2126 to provider at 2018-02-02 15:38:56.408181
February 2nd 2018, 15:38:56.727 Firetext request for 8eda51d5-cd82-4569-bfc9-d5570cdf2126 finished in 0.30376038211397827
February 2nd 2018, 15:38:49.449 sms 8eda51d5-cd82-4569-bfc9-d5570cdf2126 created at 2018-02-02 15:38:48.439113
February 2nd 2018, 15:38:49.449 sms 8eda51d5-cd82-4569-bfc9-d5570cdf2126 sent to the priority-tasks queue for delivery
To somthing like this:
February 2nd 2018, 15:39:05.885 Firetext callback return status of 0 for reference: 8eda51d5-cd82-4569-bfc9-d5570cdf2126
February 2nd 2018, 15:38:49.449 sms 8eda51d5-cd82-4569-bfc9-d5570cdf2126 created at 2018-02-02 15:38:48.439113
2018-02-02 15:55:25 +00:00
current_app . logger . debug (
2017-12-14 13:35:13 +00:00
' {} received inbound SMS with reference {} from MMG ' . format ( service . id , inbound . provider_reference ) )
return jsonify ( {
" status " : " ok "
} ) , 200
2017-05-22 11:26:47 +01:00
2017-06-20 17:13:40 +01:00
@receive_notifications_blueprint.route ( ' /notifications/sms/receive/firetext ' , methods = [ ' POST ' ] )
def receive_firetext_sms ( ) :
post_data = request . form
2017-11-17 14:36:28 +00:00
auth = request . authorization
2017-11-20 12:25:01 +00:00
if not auth :
2017-12-14 13:35:13 +00:00
current_app . logger . warning ( " Inbound sms (Firetext) no auth header " )
2017-11-23 17:17:37 +00:00
abort ( 401 )
2017-11-20 12:25:01 +00:00
elif auth . username != ' notify ' or auth . password not in current_app . config [ ' FIRETEXT_INBOUND_SMS_AUTH ' ] :
2017-12-14 13:35:13 +00:00
current_app . logger . warning ( " Inbound sms (Firetext) incorrect username ( {} ) or password " . format ( auth . username ) )
2017-11-23 17:17:37 +00:00
abort ( 403 )
2017-11-17 14:36:28 +00:00
2017-06-20 17:13:40 +01:00
inbound_number = strip_leading_forty_four ( post_data [ ' destination ' ] )
2017-08-23 13:03:52 +01:00
service = fetch_potential_service ( inbound_number , ' firetext ' )
if not service :
2017-06-20 17:13:40 +01:00
return jsonify ( {
" status " : " ok "
} ) , 200
inbound = create_inbound_sms_object ( service = service ,
content = post_data [ " message " ] ,
from_number = post_data [ ' source ' ] ,
provider_ref = None ,
date_received = post_data [ ' time ' ] ,
provider_name = " firetext " )
2020-06-29 20:31:17 +01:00
INBOUND_SMS_COUNTER . labels ( " firetext " ) . inc ( )
2017-06-20 17:13:40 +01:00
tasks . send_inbound_sms_to_service . apply_async ( [ str ( inbound . id ) , str ( service . id ) ] , queue = QueueNames . NOTIFY )
As Notify matures we probably need less logging, especially to report happy path events.
This PR is a proposal to reduce the average messages we see for a single notification from about 7 messages to 2.
Messaging would change to something like this:
February 2nd 2018, 15:39:05.885 Full delivery response from Firetext for notification: 8eda51d5-cd82-4569-bfc9-d5570cdf2126
{'status': ['0'], 'reference': ['8eda51d5-cd82-4569-bfc9-d5570cdf2126'], 'time': ['2018-02-02 15:39:01'], 'code': ['000']}
February 2nd 2018, 15:39:05.885 Firetext callback return status of 0 for reference: 8eda51d5-cd82-4569-bfc9-d5570cdf2126
February 2nd 2018, 15:38:57.727 SMS 8eda51d5-cd82-4569-bfc9-d5570cdf2126 sent to provider firetext at 2018-02-02 15:38:56.716814
February 2nd 2018, 15:38:56.727 Starting sending SMS 8eda51d5-cd82-4569-bfc9-d5570cdf2126 to provider at 2018-02-02 15:38:56.408181
February 2nd 2018, 15:38:56.727 Firetext request for 8eda51d5-cd82-4569-bfc9-d5570cdf2126 finished in 0.30376038211397827
February 2nd 2018, 15:38:49.449 sms 8eda51d5-cd82-4569-bfc9-d5570cdf2126 created at 2018-02-02 15:38:48.439113
February 2nd 2018, 15:38:49.449 sms 8eda51d5-cd82-4569-bfc9-d5570cdf2126 sent to the priority-tasks queue for delivery
To somthing like this:
February 2nd 2018, 15:39:05.885 Firetext callback return status of 0 for reference: 8eda51d5-cd82-4569-bfc9-d5570cdf2126
February 2nd 2018, 15:38:49.449 sms 8eda51d5-cd82-4569-bfc9-d5570cdf2126 created at 2018-02-02 15:38:48.439113
2018-02-02 15:55:25 +00:00
current_app . logger . debug (
2017-06-21 15:29:55 +01:00
' {} received inbound SMS with reference {} from Firetext ' . format ( service . id , inbound . provider_reference ) )
2017-06-20 17:13:40 +01:00
return jsonify ( {
" status " : " ok "
} ) , 200
2017-06-02 10:14:01 +01:00
def format_mmg_message ( message ) :
2017-11-08 13:32:30 +00:00
return unescape_string ( unquote ( message . replace ( ' + ' , ' ' ) ) )
def unescape_string ( string ) :
return string . encode ( ' raw_unicode_escape ' ) . decode ( ' unicode_escape ' )
2017-05-22 11:26:47 +01:00
2017-03-16 18:15:49 +00:00
2017-06-02 10:14:01 +01:00
def format_mmg_datetime ( date ) :
"""
We expect datetimes in format 2017 - 05 - 21 + 11 % 3 A56 % 3 A11 - ie , spaces replaced with pluses , and URI encoded
2020-04-30 14:19:08 +01:00
and in UTC
2017-06-02 10:14:01 +01:00
"""
2020-05-13 10:37:41 +01:00
try :
orig_date = format_mmg_message ( date )
parsed_datetime = iso8601 . parse_date ( orig_date ) . replace ( tzinfo = None )
return parsed_datetime
except iso8601 . ParseError :
return datetime . utcnow ( )
2017-06-02 10:14:01 +01:00
2017-06-20 17:13:40 +01:00
def create_inbound_sms_object ( service , content , from_number , provider_ref , date_received , provider_name ) :
2017-11-23 15:22:18 +00:00
user_number = try_validate_and_format_phone_number (
from_number ,
international = True ,
2021-03-25 12:53:56 +00:00
log_msg = f ' Invalid from_number received for service " { service . id } " '
2017-11-23 15:22:18 +00:00
)
2017-06-02 10:14:01 +01:00
2017-06-20 17:13:40 +01:00
provider_date = date_received
2017-06-02 10:14:01 +01:00
if provider_date :
provider_date = format_mmg_datetime ( provider_date )
2017-05-22 11:26:47 +01:00
inbound = InboundSms (
service = service ,
2017-08-14 19:47:09 +01:00
notify_number = service . get_inbound_number ( ) ,
2017-05-22 11:26:47 +01:00
user_number = user_number ,
2017-06-02 10:14:01 +01:00
provider_date = provider_date ,
2017-06-20 17:13:40 +01:00
provider_reference = provider_ref ,
content = content ,
provider = provider_name
2017-05-22 11:26:47 +01:00
)
dao_create_inbound_sms ( inbound )
return inbound
2017-05-31 16:15:25 +01:00
2017-08-23 13:03:52 +01:00
def fetch_potential_service ( inbound_number , provider_name ) :
service = dao_fetch_service_by_inbound_number ( inbound_number )
2017-06-20 17:13:40 +01:00
2017-08-23 13:03:52 +01:00
if not service :
2021-12-21 12:47:37 +00:00
current_app . logger . warning ( ' Inbound number " {} " from {} not associated with a service ' . format (
2017-06-20 17:13:40 +01:00
inbound_number , provider_name
2017-06-02 15:58:36 +01:00
) )
2017-06-20 17:13:40 +01:00
return False
2017-06-29 15:33:44 +01:00
2017-08-23 13:03:52 +01:00
if not has_inbound_sms_permissions ( service . permissions ) :
2017-06-29 15:33:44 +01:00
current_app . logger . error (
2017-08-23 13:03:52 +01:00
' Service " {} " does not allow inbound SMS ' . format ( service . id ) )
2017-06-29 15:33:44 +01:00
return False
2017-08-23 13:03:52 +01:00
return service
2017-06-06 11:50:30 +01:00
2017-07-05 15:08:53 +01:00
def has_inbound_sms_permissions ( permissions ) :
str_permissions = [ p . permission for p in permissions ]
return set ( [ INBOUND_SMS_TYPE , SMS_TYPE ] ) . issubset ( set ( str_permissions ) )
2017-06-06 11:50:30 +01:00
def strip_leading_forty_four ( number ) :
if number . startswith ( ' 44 ' ) :
return number . replace ( ' 44 ' , ' 0 ' , 1 )
return number