2016-04-18 09:55:56 +01:00
|
|
|
import json
|
2018-05-04 10:55:58 +01:00
|
|
|
from time import monotonic
|
2016-09-22 17:18:05 +01:00
|
|
|
from requests import (request, RequestException)
|
|
|
|
|
from app.clients.sms import (SmsClient, SmsClientResponseException)
|
2016-04-04 15:02:21 +01:00
|
|
|
|
2016-04-06 14:31:33 +01:00
|
|
|
mmg_response_map = {
|
2020-05-27 18:03:55 +01:00
|
|
|
'2': {'status': 'permanent-failure', 'substatus': {
|
|
|
|
|
"1": "Number does not exist",
|
|
|
|
|
"4": "Rejected by operator",
|
|
|
|
|
"5": "Unidentified Subscriber",
|
|
|
|
|
"9": "Undelivered",
|
|
|
|
|
"11": "Service for Subscriber suspended",
|
|
|
|
|
"12": "Illegal equipment",
|
|
|
|
|
"2049": "Subscriber IMSI blacklisted",
|
|
|
|
|
"2050": "Number blacklisted in do-not-disturb blacklist",
|
|
|
|
|
"2052": "Destination number blacklisted",
|
|
|
|
|
"2053": "Source address blacklisted"
|
|
|
|
|
}},
|
|
|
|
|
'3': {'status': 'delivered', 'substatus': {"2": "Delivered to operator", "5": "Delivered to handset"}},
|
|
|
|
|
'4': {'status': 'temporary-failure', 'substatus': {
|
|
|
|
|
"6": "Absent Subscriber",
|
|
|
|
|
"8": "Roaming not allowed",
|
|
|
|
|
"13": "SMS Not Supported",
|
|
|
|
|
"15": "Expired",
|
|
|
|
|
"27": "Absent Subscriber",
|
|
|
|
|
"29": "Invalid delivery report",
|
|
|
|
|
"32": "Delivery Failure",
|
|
|
|
|
}},
|
|
|
|
|
'5': {'status': 'permanent-failure', 'substatus': {
|
|
|
|
|
"6": "Network out of coverage",
|
|
|
|
|
"8": "Incorrect number prefix",
|
|
|
|
|
"10": "Number on do-not-disturb service",
|
|
|
|
|
"11": "Sender id not registered",
|
|
|
|
|
"13": "Sender id blacklisted",
|
|
|
|
|
"14": "Destination number blacklisted",
|
|
|
|
|
"19": "Routing unavailable",
|
|
|
|
|
"20": "Rejected by anti-flooding mechanism",
|
|
|
|
|
"21": "System error", # it says to retry those messages or contact support
|
|
|
|
|
"23": "Duplicate message id",
|
|
|
|
|
"24": "Message formatted incorrectly",
|
|
|
|
|
"25": "Message too long",
|
|
|
|
|
"51": "Missing recipient value",
|
|
|
|
|
"52": "Invalid destination",
|
|
|
|
|
}},
|
2016-04-06 14:31:33 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2020-06-01 11:45:35 +01:00
|
|
|
def get_mmg_responses(status, detailed_status_code=None):
|
|
|
|
|
return (mmg_response_map[status]["status"], mmg_response_map[status]["substatus"].get(detailed_status_code, None))
|
2016-04-04 15:02:21 +01:00
|
|
|
|
|
|
|
|
|
2016-09-22 17:18:05 +01:00
|
|
|
class MMGClientResponseException(SmsClientResponseException):
|
|
|
|
|
def __init__(self, response, exception):
|
2017-04-05 15:31:37 +01:00
|
|
|
status_code = response.status_code if response is not None else 504
|
|
|
|
|
text = response.text if response is not None else "Gateway Time-out"
|
|
|
|
|
|
|
|
|
|
self.status_code = status_code
|
|
|
|
|
self.text = text
|
2016-09-22 17:18:05 +01:00
|
|
|
self.exception = exception
|
2016-04-04 15:02:21 +01:00
|
|
|
|
|
|
|
|
def __str__(self):
|
2016-09-22 17:18:05 +01:00
|
|
|
return "Code {} text {} exception {}".format(self.status_code, self.text, str(self.exception))
|
2016-04-01 16:42:31 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class MMGClient(SmsClient):
|
|
|
|
|
'''
|
|
|
|
|
MMG sms client
|
|
|
|
|
'''
|
|
|
|
|
|
2016-06-01 15:24:19 +01:00
|
|
|
def init_app(self, current_app, statsd_client, *args, **kwargs):
|
2016-04-07 10:53:59 +01:00
|
|
|
super(SmsClient, self).__init__(*args, **kwargs)
|
2016-06-01 15:24:19 +01:00
|
|
|
self.current_app = current_app
|
|
|
|
|
self.api_key = current_app.config.get('MMG_API_KEY')
|
2016-06-06 09:49:51 +01:00
|
|
|
self.from_number = current_app.config.get('FROM_NUMBER')
|
2016-04-21 11:37:38 +01:00
|
|
|
self.name = 'mmg'
|
2016-05-13 17:15:39 +01:00
|
|
|
self.statsd_client = statsd_client
|
2016-07-20 10:40:50 +01:00
|
|
|
self.mmg_url = current_app.config.get('MMG_URL')
|
2016-04-01 16:42:31 +01:00
|
|
|
|
2016-09-22 17:18:05 +01:00
|
|
|
def record_outcome(self, success, response):
|
2017-04-05 15:31:37 +01:00
|
|
|
status_code = response.status_code if response else 503
|
2016-09-23 10:24:12 +01:00
|
|
|
log_message = "API {} request {} on {} response status_code {}".format(
|
2016-09-22 17:18:05 +01:00
|
|
|
"POST",
|
|
|
|
|
"succeeded" if success else "failed",
|
|
|
|
|
self.mmg_url,
|
2017-04-05 15:31:37 +01:00
|
|
|
status_code
|
2016-09-22 17:18:05 +01:00
|
|
|
)
|
|
|
|
|
|
2016-09-23 15:59:23 +01:00
|
|
|
if success:
|
2016-09-22 17:18:05 +01:00
|
|
|
self.current_app.logger.info(log_message)
|
|
|
|
|
self.statsd_client.incr("clients.mmg.success")
|
2016-09-23 15:59:23 +01:00
|
|
|
else:
|
|
|
|
|
self.statsd_client.incr("clients.mmg.error")
|
2021-01-18 15:43:50 +00:00
|
|
|
self.current_app.logger.warning(log_message)
|
2016-09-22 17:18:05 +01:00
|
|
|
|
2016-04-01 16:42:31 +01:00
|
|
|
def get_name(self):
|
|
|
|
|
return self.name
|
|
|
|
|
|
2016-07-01 14:06:32 +01:00
|
|
|
def send_sms(self, to, content, reference, multi=True, sender=None):
|
2016-04-01 16:42:31 +01:00
|
|
|
data = {
|
|
|
|
|
"reqType": "BULK",
|
|
|
|
|
"MSISDN": to,
|
|
|
|
|
"msg": content,
|
2016-08-23 12:05:47 +01:00
|
|
|
"sender": self.from_number if sender is None else sender,
|
2016-04-18 09:55:56 +01:00
|
|
|
"cid": reference,
|
|
|
|
|
"multi": multi
|
2016-04-01 16:42:31 +01:00
|
|
|
}
|
|
|
|
|
|
add raw request timings to provider send functions
we're using statsd to monitor how long provider requests are taking.
However, there's lots of busy work that happens inside our statsd
metrics timing window. Things like json dumping and loading, building
headers, exception handling, etc.
for firetext/mmg, the response object from requests has an elapsed
property [1], which captures from sending raw data to parsing the
response headers. for ses, it's a bit trickier, but boto3 exposes a few
event hooks [2]. it's hard to find them without stepping through the
code, but the interesting ones are before-call, after-call,
after-call-error, request-created, and response-received. The
before-call and after-call involve some marshalling, built-in retrying,
etc, while request-created and response-received are much lower level.
They might be called more than once per ses request, if boto3 itself
retries the request on 5xx, 429 and low level socket errors [3].
Add these as new `raw-request-time` metrics rather than overwriting to
avoid changing the meaning of an existing metric, and to let us compare
the metrics to see if there's a noticeable difference at all
[1] https://requests.readthedocs.io/en/master/api/#requests.Response.elapsed
[2] https://boto3.amazonaws.com/v1/documentation/api/latest/guide/events.html
[3] https://boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html#legacy-retry-mode
2020-05-29 13:56:29 +01:00
|
|
|
response = None
|
2016-04-01 16:42:31 +01:00
|
|
|
start_time = monotonic()
|
|
|
|
|
try:
|
2016-09-22 17:18:05 +01:00
|
|
|
response = request(
|
|
|
|
|
"POST",
|
|
|
|
|
self.mmg_url,
|
|
|
|
|
data=json.dumps(data),
|
|
|
|
|
headers={
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'Authorization': 'Basic {}'.format(self.api_key)
|
2017-04-04 16:05:25 +01:00
|
|
|
},
|
|
|
|
|
timeout=60
|
2016-06-01 15:24:19 +01:00
|
|
|
)
|
2016-09-22 17:18:05 +01:00
|
|
|
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
try:
|
|
|
|
|
json.loads(response.text)
|
|
|
|
|
except (ValueError, AttributeError) as e:
|
|
|
|
|
self.record_outcome(False, response)
|
|
|
|
|
raise MMGClientResponseException(response=response, exception=e)
|
|
|
|
|
self.record_outcome(True, response)
|
2016-04-01 16:42:31 +01:00
|
|
|
except RequestException as e:
|
2016-09-22 17:18:05 +01:00
|
|
|
self.record_outcome(False, e.response)
|
|
|
|
|
raise MMGClientResponseException(response=e.response, exception=e)
|
2016-04-01 16:42:31 +01:00
|
|
|
finally:
|
|
|
|
|
elapsed_time = monotonic() - start_time
|
2016-08-08 11:23:58 +01:00
|
|
|
self.statsd_client.timing("clients.mmg.request-time", elapsed_time)
|
add raw request timings to provider send functions
we're using statsd to monitor how long provider requests are taking.
However, there's lots of busy work that happens inside our statsd
metrics timing window. Things like json dumping and loading, building
headers, exception handling, etc.
for firetext/mmg, the response object from requests has an elapsed
property [1], which captures from sending raw data to parsing the
response headers. for ses, it's a bit trickier, but boto3 exposes a few
event hooks [2]. it's hard to find them without stepping through the
code, but the interesting ones are before-call, after-call,
after-call-error, request-created, and response-received. The
before-call and after-call involve some marshalling, built-in retrying,
etc, while request-created and response-received are much lower level.
They might be called more than once per ses request, if boto3 itself
retries the request on 5xx, 429 and low level socket errors [3].
Add these as new `raw-request-time` metrics rather than overwriting to
avoid changing the meaning of an existing metric, and to let us compare
the metrics to see if there's a noticeable difference at all
[1] https://requests.readthedocs.io/en/master/api/#requests.Response.elapsed
[2] https://boto3.amazonaws.com/v1/documentation/api/latest/guide/events.html
[3] https://boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html#legacy-retry-mode
2020-05-29 13:56:29 +01:00
|
|
|
if response and hasattr(response, 'elapsed'):
|
|
|
|
|
self.statsd_client.timing("clients.mmg.raw-request-time", response.elapsed.total_seconds())
|
|
|
|
|
|
2016-12-20 13:24:08 +00:00
|
|
|
self.current_app.logger.info("MMG request for {} finished in {}".format(reference, elapsed_time))
|
2016-09-22 17:18:05 +01:00
|
|
|
|
2016-04-01 16:42:31 +01:00
|
|
|
return response
|