Browse Source

Support config for SMS originators

David Baker 7 years ago
parent
commit
a812f59603
4 changed files with 82 additions and 9 deletions
  1. 10 0
      README.rst
  2. 2 1
      sydent/http/servlets/msisdnservlet.py
  3. 15 2
      sydent/sms/openmarket.py
  4. 55 6
      sydent/validators/msisdnvalidator.py

+ 10 - 0
README.rst

@@ -9,6 +9,16 @@ Having installed dependencies, you can run sydent using::
 
 This will create a configuration file in sydent.conf with some defaults. You'll most likely want to change the server name and specify a mail relay.
 
+Defaults for SMS originators will not be added to the generated config file, these should be added in the form::
+
+    originators.<country code> = <long|short|alpha>:<originator>
+
+Where country code is the numeric country code, or 'default' to specify the originator used for countries not listed. For example, to use a selection of long codes for the US/Canda, a short code for the UK and an alphanumertic originator for everywhere else::
+
+    originators.1 = long:12125552368,long:12125552369
+    originators.44 = short:12345
+    originators.default = alpha:Matrix
+
 Requests
 ========
 

+ 2 - 1
sydent/http/servlets/msisdnservlet.py

@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 
 # Copyright 2016 OpenMarket Ltd
+# Copyright 2017 Vector Creations Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -67,7 +68,7 @@ class MsisdnRequestCodeServlet(Resource):
 
         try:
             sid = self.sydent.validators.msisdn.requestToken(
-                msisdn, clientSecret, sendAttempt, None
+                phone_number_object, clientSecret, sendAttempt, None
             )
         except Exception as e:
             logger.error("Exception sending SMS: %r", e);

+ 15 - 2
sydent/sms/openmarket.py

@@ -30,6 +30,18 @@ API_BASE_URL = "https://smsc.openmarket.com/sms/v4/mt"
 # Useful for testing.
 #API_BASE_URL = "http://smsc-cie.openmarket.com/sms/v4/mt"
 
+TONS = {
+    'long': 1,
+    'short': 3,
+    'alpha': 5,
+}
+
+def tonFromType(t):
+    if t in TONS:
+        return TONS[t]
+    raise Exception("Unknown number type (%s) for originator" % t)
+
+
 class OpenMarketSMS:
     def __init__(self, sydent):
         self.sydent = sydent
@@ -49,8 +61,9 @@ class OpenMarketSMS:
             },
         }
         if source:
-            body['source'] = {
-                "address": source,
+            body['mobileTerminate']['source'] = {
+                "ton": tonFromType(source['type']),
+                "address": source['text'],
             }
 
         b64creds = b64encode(b"%s:%s" % (

+ 55 - 6
sydent/validators/msisdnvalidator.py

@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 
 # Copyright 2016 OpenMarket Ltd
+# Copyright 2017 Vector Creations Ltd
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -16,6 +17,7 @@
 
 import logging
 import urllib
+import phonenumbers
 
 from sydent.db.valsession import ThreePidValSessionStore
 from sydent.validators import ValidationSession, common
@@ -31,9 +33,33 @@ class MsisdnValidator:
         self.sydent = sydent
         self.omSms = OpenMarketSMS(sydent)
 
-    def requestToken(self, msisdn, clientSecret, sendAttempt, nextLink):
+        # cache originators from config file
+        self.originators = {}
+        for opt in self.sydent.cfg.options('sms'):
+            if opt.startswith('originators.'):
+                country = opt.split('.')[1]
+                rawVal = self.sydent.cfg.get('sms', opt)
+                rawList = [i.strip() for i in rawVal.split(',')]
+
+                self.originators[country] = []
+                for origString in rawList:
+                    parts = origString.split(':')
+                    if len(parts) != 2:
+                        raise Exception("Originators must be in form: long:<number>, short:<number> or alpha:<text>, separated by commas")
+                    if parts[0] not in ['long', 'short', 'alpha']:
+                        raise Exception("Invalid originator type: valid types are long, short and alpha")
+                    self.originators[country].append({
+                        "type": parts[0],
+                        "text": parts[1],
+                    })
+
+    def requestToken(self, phoneNumber, clientSecret, sendAttempt, nextLink):
         valSessionStore = ThreePidValSessionStore(self.sydent)
 
+        msisdn = phonenumbers.format_number(
+            phoneNumber, phonenumbers.PhoneNumberFormat.E164
+        )[1:]
+
         valSession = valSessionStore.getOrCreateTokenSession(
             medium='msisdn', address=msisdn, clientSecret=clientSecret
         )
@@ -44,20 +70,43 @@ class MsisdnValidator:
             logger.info("Not texting code because current send attempt (%d) is not less than given send attempt (%s)", int(sendAttempt), int(valSession.sendAttemptNumber))
             return valSession.id
 
+        smsBodyTemplate = self.sydent.cfg.get('sms', 'bodyTemplate')
+        originator = self.getOriginator(phoneNumber)
+
         logger.info(
-            "Attempting to text code %s to %s",
-            valSession.token, msisdn,
+            "Attempting to text code %s to %s (country %d) with originator %s",
+            valSession.token, msisdn, phoneNumber.country_code, originator
         )
 
-        smsBodyTemplate = self.sydent.cfg.get('sms', 'bodyTemplate')
-
         smsBody = smsBodyTemplate.format(token=valSession.token)
 
-        self.omSms.sendTextSMS(smsBody, msisdn)
+        self.omSms.sendTextSMS(smsBody, msisdn, originator)
 
         valSessionStore.setSendAttemptNumber(valSession.id, sendAttempt)
 
         return valSession.id
 
+    def getOriginator(self, destPhoneNumber):
+        countryCode = str(destPhoneNumber.country_code)
+
+        origs = [{
+            "type": "alpha",
+            "text": "Matrix",
+        }]
+        if countryCode in self.originators:
+            origs = self.originators[countryCode]
+        elif 'default' in self.originators:
+            origs = self.originators['default']
+
+        # deterministically pick an originator from the list of possible
+        # originators, so if someone requests multiple codes, they come from
+        # a consistent number (if there's any chance that some originators are
+        # more likley to work than others, we may want to change, but it feels
+        # like this should be something other than just picking one randomly).
+        msisdn = phonenumbers.format_number(
+            destPhoneNumber, phonenumbers.PhoneNumberFormat.E164
+        )[1:]
+        return origs[sum([int(i) for i in msisdn]) % len(origs)]
+
     def validateSessionWithToken(self, sid, clientSecret, token):
         return common.validateSessionWithToken(self.sydent, sid, clientSecret, token)