Skip to content

Commit

Permalink
Merge pull request #42 from matrix-org/dbkr/originators
Browse files Browse the repository at this point in the history
Support config for SMS originators
  • Loading branch information
dbkr authored Apr 20, 2017
2 parents 2d60e73 + 93cb5fa commit bb9e3a3
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 9 deletions.
10 changes: 10 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
========

Expand Down
3 changes: 2 additions & 1 deletion sydent/http/servlets/msisdnservlet.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -67,7 +68,7 @@ def render_POST(self, request):

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);
Expand Down
18 changes: 16 additions & 2 deletions sydent/sms/openmarket.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@
# Useful for testing.
#API_BASE_URL = "http://smsc-cie.openmarket.com/sms/v4/mt"

# The TON (ie. Type of Number) codes by type used in our config file
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
Expand All @@ -49,8 +62,9 @@ def sendTextSMS(self, body, dest, source=None):
},
}
if source:
body['source'] = {
"address": source,
body['mobileTerminate']['source'] = {
"ton": tonFromType(source['type']),
"address": source['text'],
}

b64creds = b64encode(b"%s:%s" % (
Expand Down
61 changes: 55 additions & 6 deletions sydent/validators/msisdnvalidator.py
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -16,6 +17,7 @@

import logging
import urllib
import phonenumbers

from sydent.db.valsession import ThreePidValSessionStore
from sydent.validators import ValidationSession, common
Expand All @@ -31,9 +33,33 @@ def __init__(self, sydent):
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
)
Expand All @@ -44,20 +70,43 @@ def requestToken(self, msisdn, clientSecret, sendAttempt, nextLink):
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)

0 comments on commit bb9e3a3

Please sign in to comment.